Udara Jakarta Bercerita: 10 Tahun Data yang Tak Bisa
Dibantah
Turning Data into Action: Visualizing
Insights for a Sustainable Future
Subtema: Kualitas Udara Perkotaan — ISPU DKI Jakarta
2015–2024
library(tidyverse)
library(lubridate)
library(scales)
library(reshape2)
library(ggtext)
library(patchwork)
library(kableExtra)
library(corrplot)
library(readxl)ISPU (Indeks Standar Pencemaran Udara) adalah indeks komposit yang mengukur tingkat pencemaran udara berdasarkan enam parameter: PM10, PM2.5, SO₂, CO, O₃, dan NO₂. Analisis ini menggunakan data dari 5 Stasiun Pemantauan Kualitas Udara (SPKU) milik Dinas Lingkungan Hidup DKI Jakarta selama periode 2015–2024, mencakup sekitar 3.300 hari pengukuran per stasiun.
Catatan Cakupan Data: Dataset sumber sebenarnya tersedia sejak 2010, namun analisis ini dipersempit ke 2015–2024 karena periode 2010–2014 memiliki cakupan stasiun yang belum lengkap (jumlah hari terekam jauh di bawah baseline tahun-tahun setelahnya) sehingga kurang adil untuk dibandingkan, dan karena satu dekade terakhir lebih relevan untuk konteks kebijakan kualitas udara Jakarta saat ini.
Skala Kategori ISPU:
🟢 BAIK (0–50): Kualitas udara memuaskan, polusi udara
tidak menimbulkan risiko.
🟡 SEDANG (51–100): Kualitas udara dapat diterima,
namun beberapa polutan memiliki efek bagi kelompok sensitif.
🟠 TIDAK SEHAT (101–199): Anggota kelompok sensitif
dapat mengalami efek kesehatan; masyarakat umum tidak terpengaruh.
🔴 SANGAT TIDAK SEHAT (200–299): Risiko kesehatan
meningkat untuk semua orang.
🟣 BERBAHAYA (300+): Darurat kesehatan; seluruh
populasi berisiko.
# ============================================================
# CATATAN: Letakkan file Excel di direktori yang sama dengan
# file Rmd ini, lalu set working directory ke sana.
# Dataset ini adalah hasil penggabungan seluruh data SPKU
# 5 stasiun (2015–2024) + data tahun 2022, yang sudah
# dibersihkan dan diseragamkan formatnya.
# ============================================================
# --- Baca data gabungan dari Excel ---
# PENTING: guess_max diset besar (melebihi jumlah baris) agar readxl
# membaca SELURUH baris sebelum menebak tipe kolom. Tanpa ini, kolom
# yang nilainya jarang muncul di awal (mis. pm25, baru terisi mulai
# baris ~17.700 karena data 2021+) akan salah dideteksi sebagai
# kolom logical (TRUE/NA) alih-alih numeric, dan nilai aslinya hilang.
data_raw <- read_excel("data_ispu_dki_2010_2024.xlsx", sheet = "Sheet 1",
guess_max = 100000)
# --- Normalisasi tipe data & kolom turunan ---
# Kolom tahun/bulan_n/musim sudah tersedia di file sumber, tetapi
# kita hitung ulang dari `tanggal` agar konsisten & tidak bergantung
# pada penamaan kolom yang mungkin berbeda di file lain di kemudian hari.
data_raw <- data_raw %>%
mutate(
tanggal = as.Date(tanggal),
tahun = year(tanggal),
bulan = month(tanggal, label = TRUE, abbr = FALSE),
bulan_n = month(tanggal),
musim = ifelse(bulan_n %in% 5:10, "Kemarau (Mei–Okt)", "Hujan (Nov–Apr)"),
critical = recode(critical, "PM2,5" = "PM25") # samakan alias penulisan
)
# --- Bersihkan: buang data di luar rentang & nilai tidak valid ---
# REVISI: rentang dipersempit ke 2015-2024 (bukan 2010-2024) karena
# 2010-2011 cakupan stasiun belum lengkap (4/5 SPKU, hari terekam jauh
# di bawah baseline ~1.780-1.820 hari/tahun yang konsisten sejak 2015),
# dan agar analisis lebih relevan dengan konteks kebijakan 10 tahun
# terakhir. Tahun 2022 tetap disertakan walau juga tidak lengkap secara
# cakupan (lihat chunk `cek-cakupan-2022` di bawah untuk detail & alasan
# tetap mempertahankannya alih-alih membuang.
data_clean <- data_raw %>%
filter(tahun >= 2015, tahun <= 2024, max > 0, !is.na(max), !is.na(categori))
# --- Palette warna global ---
warna_kategori <- c(
"BAIK" = "#27ae60",
"SEDANG" = "#f1c40f",
"TIDAK SEHAT" = "#e67e22",
"SANGAT TIDAK SEHAT" = "#e74c3c",
"BERBAHAYA" = "#8e44ad"
)
kategori_order <- c("BAIK", "SEDANG", "TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA")
warna_polutan <- c(
"O3" = "#9b59b6",
"PM10" = "#e74c3c",
"PM25" = "#e67e22",
"PM2,5" = "#e67e22", # alias dari data 2022
"CO" = "#3498db",
"SO2" = "#1abc9c",
"NO2" = "#2c3e50"
)
cat("Data siap:", nrow(data_clean), "baris | Periode:",
min(data_clean$tahun), "–", max(data_clean$tahun), "\n")## Data siap: 16543 baris | Periode: 2015 – 2024
# --- Angka-angka kunci untuk referensi di seluruh dokumen ---
total_hari <- nrow(data_clean)
hari_baik <- sum(data_clean$categori == "BAIK", na.rm = TRUE)
hari_sedang <- sum(data_clean$categori == "SEDANG", na.rm = TRUE)
hari_tdk_sehat <- sum(data_clean$categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"), na.rm = TRUE)
hari_berbahaya <- sum(data_clean$categori == "BERBAHAYA", na.rm = TRUE)
persen_baik <- hari_baik / total_hari * 100
persen_sedang <- hari_sedang / total_hari * 100
persen_tdk_baik <- (total_hari - hari_baik) / total_hari * 100
persen_buruk <- hari_tdk_sehat / total_hari * 100
# Polutan dominan terbanyak
polutan_dom <- data_clean %>%
filter(!is.na(critical)) %>%
count(critical, sort = TRUE) %>%
slice(1)
# Tahun terburuk & terbaik
year_stats <- data_clean %>%
group_by(tahun) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop")
tahun_terburuk <- year_stats$tahun[which.max(year_stats$rata)]
ispu_terburuk <- round(max(year_stats$rata), 1)
tahun_terbaik <- year_stats$tahun[which.min(year_stats$rata)]
ispu_terbaik <- round(min(year_stats$rata), 1)
# Stasiun terburuk & terbersih
stasiun_stats <- data_clean %>%
group_by(stasiun) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop") %>%
arrange(desc(rata))
stasiun_terburuk <- stasiun_stats$stasiun[1]
ispu_st_terburuk <- round(stasiun_stats$rata[1], 1)
stasiun_terbaik <- stasiun_stats$stasiun[nrow(stasiun_stats)]
ispu_st_terbaik <- round(stasiun_stats$rata[nrow(stasiun_stats)], 1)
# Bulan terburuk
bulan_stats <- data_clean %>%
group_by(bulan_n) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop")
bulan_terburuk_n <- bulan_stats$bulan_n[which.max(bulan_stats$rata)]
bulan_terburuk <- format(as.Date(paste0("2024-", bulan_terburuk_n, "-01")), "%B")# Cek kelengkapan cakupan data per tahun. Baseline normal ~1.780-1.820
# hari/tahun (5 stasiun x ~360 hari). Tahun 2022 jauh di bawah itu dan
# didominasi satu stasiun, sehingga dikecualikan dari perhitungan
# "tahun terburuk" agar perbandingan antar tahun tetap adil.
cakupan_tahun <- data_clean %>%
group_by(tahun) %>%
summarise(jumlah_hari = n(), jumlah_stasiun = n_distinct(stasiun), .groups = "drop")
baseline_cakupan <- median(cakupan_tahun$jumlah_hari[cakupan_tahun$tahun != 2022])
tahun_tdk_lengkap <- cakupan_tahun %>%
filter(jumlah_hari < 0.7 * baseline_cakupan) %>% pull(tahun)
detail_stasiun_2022 <- data_clean %>%
filter(tahun == 2022) %>%
count(stasiun, sort = TRUE)
stasiun_dom_2022 <- detail_stasiun_2022$stasiun[1]
hari_dom_2022 <- detail_stasiun_2022$n[1]
hari_2022_total <- sum(detail_stasiun_2022$n)
year_stats_lengkap <- data_clean %>%
filter(!tahun %in% tahun_tdk_lengkap) %>%
group_by(tahun) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop")
tahun_terburuk_lengkap <- year_stats_lengkap$tahun[which.max(year_stats_lengkap$rata)]
ispu_terburuk_lengkap <- round(max(year_stats_lengkap$rata), 1)Catatan: cakupan observasi 2022 tidak selengkap tahun lain (335 hari, dominan dari stasiun Lubang Buaya), sehingga klaim “tahun terburuk” pada dokumen ini dihitung dari tahun-tahun dengan cakupan lengkap.
*Cakupan 2022 tidak lengkap, sehingga dikecualikan dari perhitungan ini.
Temuan Utama: Selama 10 tahun (2015–2024), udara Jakarta tidak pernah secara konsisten berada di zona BAIK. Lebih dari tiga perempat hari, warga Jakarta menghirup udara berkategori SEDANG hingga berbahaya. Pandemi COVID-19 tahun 2020 menjadi satu-satunya “eksperimen alam” yang membuktikan bahwa pembatasan aktivitas manusia dapat menurunkan polusi secara dramatis — namun efeknya tidak bertahan lama.
distribusi_kat <- data_clean %>%
count(categori) %>%
mutate(
persen = n / sum(n) * 100,
categori = factor(categori, levels = kategori_order),
label = ifelse(persen > 2, paste0(round(persen, 1), "%"), "")
) %>%
filter(!is.na(categori)) # buang "TIDAK ADA DATA" jika masih tersisa
ggplot(distribusi_kat, aes(x = 2, y = persen, fill = categori)) +
geom_col(width = 1, color = "white", linewidth = 1) +
coord_polar("y", start = 0) +
xlim(0.5, 2.5) +
scale_fill_manual(values = warna_kategori, name = "Kategori ISPU") +
geom_text(aes(label = label),
position = position_stack(vjust = 0.5),
color = "white", fontface = "bold", size = 4.5) +
annotate("text", x = 0.5, y = 0,
label = paste0(round(persen_tdk_baik, 0), "%\nhari tidak\noptimal"),
size = 6, fontface = "bold", color = "#2c3e50") +
theme_void(base_size = 13) +
theme(legend.position = "right") +
labs(
title = "Distribusi Kualitas Udara Jakarta (2015–2024)",
subtitle = paste0("Hanya ", round(persen_baik, 0),
"% hari benar-benar BAIK — lebih dari 3/4 hari di zona kuning atau lebih buruk"),
caption = "Sumber: DLH DKI Jakarta"
)Interpretasi: Donut chart ini mengungkap realita mendasar kualitas udara Jakarta. Kategori SEDANG mendominasi dengan proporsi terbesar, menandakan bahwa kondisi “lumayan aman” justru menjadi norma harian. Ini berbahaya karena paparan jangka panjang terhadap udara berkategori SEDANG tetap berdampak pada kelompok sensitif — anak-anak, lansia, ibu hamil, dan penderita penyakit pernapasan. Hanya 17% hari yang benar-benar memenuhi standar BAIK, berarti hampir 83% hari warga Jakarta terpapar kualitas udara yang tidak ideal. Meski hari BERBAHAYA sangat jarang, keberadaannya (0 hari) menunjukkan Jakarta pernah mencapai kondisi darurat udara.
trend_tahun <- data_clean %>%
group_by(tahun) %>%
summarise(
rata_ispu = mean(max, na.rm = TRUE),
median_ispu = median(max, na.rm = TRUE),
persen_buruk = mean(categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"),
na.rm = TRUE) * 100,
persen_baik = mean(categori == "BAIK", na.rm = TRUE) * 100,
jumlah_hari = n(),
.groups = "drop"
)
# Variabel untuk inline reference
ispu_2019 <- filter(trend_tahun, tahun == 2019)$rata_ispu
ispu_2020 <- filter(trend_tahun, tahun == 2020)$rata_ispu
ispu_2021 <- filter(trend_tahun, tahun == 2021)$rata_ispu
penurunan_pct <- (ispu_2019 - ispu_2020) / ispu_2019 * 100
rebound_pct <- (ispu_2021 - ispu_2020) / ispu_2020 * 100p_tren <- ggplot(trend_tahun, aes(x = tahun, y = rata_ispu)) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 100, ymax = Inf,
fill = "#e74c3c", alpha = 0.08) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 50, ymax = 100,
fill = "#f39c12", alpha = 0.08) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = 50,
fill = "#27ae60", alpha = 0.08) +
geom_hline(yintercept = c(50, 100),
linetype = "dashed",
color = c("#27ae60", "#e74c3c"),
linewidth = 0.6) +
annotate("text", x = 2015.2, y = 103,
label = "TIDAK SEHAT (>100)", color = "#e74c3c",
size = 3, hjust = 0, fontface = "italic") +
annotate("text", x = 2015.2, y = 75,
label = "SEDANG (51–100)", color = "#e67e22",
size = 3, hjust = 0, fontface = "italic") +
annotate("text", x = 2015.2, y = 46,
label = "BAIK (≤50)", color = "#27ae60",
size = 3, hjust = 0, fontface = "italic") +
geom_line(color = "#2c3e50", linewidth = 1.1) +
geom_point(color = "#2c3e50", size = 3, fill = "white", shape = 21, stroke = 1.2) +
# Sorot 2020 (COVID)
geom_point(data = filter(trend_tahun, tahun == 2020),
color = "#27ae60", size = 6, shape = 19) +
geom_text(data = filter(trend_tahun, tahun == 2020),
aes(label = paste0("COVID-19\n↓", round(penurunan_pct, 0), "%")),
vjust = 2.2, size = 3.5, color = "#27ae60", fontface = "bold") +
# Sorot tahun terburuk versi cakupan-lengkap (dinamis, bukan tahun tetap)
geom_point(data = filter(trend_tahun, tahun == tahun_terburuk_lengkap),
color = "#e74c3c", size = 6, shape = 19) +
geom_text(data = filter(trend_tahun, tahun == tahun_terburuk_lengkap),
aes(label = paste0("Terburuk*\n", round(rata_ispu, 1))),
vjust = -1.5, size = 3.5, color = "#e74c3c", fontface = "bold") +
# Tandai 2022 secara eksplisit: lingkaran putus-putus abu-abu + label,
# supaya jelas kenapa titik ini TIDAK diberi label "Terburuk" walau
# nilainya lebih tinggi -- datanya tidak lengkap (lihat catatan di atas).
geom_point(data = filter(trend_tahun, tahun == 2022),
color = "#7f8c8d", size = 7, shape = 21, stroke = 1.3,
fill = NA, linetype = "dashed") +
geom_text(data = filter(trend_tahun, tahun == 2022),
aes(label = paste0(round(rata_ispu, 1), "\n(data tidak lengkap)")),
vjust = -1.3, size = 3, color = "#7f8c8d", fontface = "italic") +
scale_x_continuous(breaks = seq(2015, 2024, 1)) +
scale_y_continuous(limits = c(42, max(trend_tahun$rata_ispu) + 10)) +
labs(x = "Tahun", y = "Rata-rata ISPU (maks. harian)",
title = "Tren ISPU DKI Jakarta (2015–2024)",
subtitle = paste0("Fluktuatif tanpa perbaikan struktural; 2020 titik terendah akibat PSBB, ",
tahun_terburuk_lengkap, " terburuk dalam 10 tahun (data lengkap)"),
caption = "Sumber: DLH DKI Jakarta") +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "#555555", size = 11))
p_bar <- ggplot(trend_tahun, aes(x = factor(tahun), y = persen_buruk)) +
geom_col(aes(fill = persen_buruk), width = 0.65, show.legend = FALSE) +
geom_text(aes(label = paste0(round(persen_buruk, 1), "%")),
vjust = -0.4, size = 3, fontface = "bold") +
scale_fill_gradient(low = "#f9e79f", high = "#c0392b") +
scale_y_continuous(labels = function(x) paste0(x, "%"),
limits = c(0, max(trend_tahun$persen_buruk) + 6)) +
labs(x = "Tahun", y = "% Hari Tidak Sehat",
title = "Persentase Hari dengan Udara Tidak Sehat per Tahun") +
theme_minimal(base_size = 11) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 8),
panel.grid.minor = element_blank())
p_tren / p_bar + plot_layout(heights = c(1.6, 1))Interpretasi: Tren 10 tahun menunjukkan pola fluktuatif tanpa perbaikan struktural yang konsisten. Tiga fase utama dapat diidentifikasi:
2015–2019 (Fluktuatif Pra-Pandemi): ISPU bergerak naik-turun di kisaran 65–81 tanpa arah tren yang jelas, mencerminkan absennya kebijakan lingkungan jangka panjang yang benar-benar menekan polusi secara struktural. Fase ini mencapai titik tertinggi pra-pandemi pada 2019 (rata-rata ISPU 80.9).
2020 (Anomali PSBB): Titik terendah sepanjang periode 2015–2024 terjadi di 2020 saat PSBB COVID-19 membatasi mobilitas, terbukti menurunkan ISPU 22% dibandingkan 2019.
2021–2024 (Rebound Pasca-Pandemi): Begitu pembatasan dicabut, ISPU kembali melonjak melampaui level pra-pandemi. Tahun 2019 tercatat sebagai yang tertinggi (rata-rata ISPU 80.9), mengonfirmasi bahwa penurunan polusi selama pandemi bersifat sementara, bukan hasil perbaikan struktural — ISPU tahun-tahun setelahnya konsisten kembali ke level pra-pandemi atau lebih tinggi.
Catatan Penting: Penurunan ISPU saat COVID bukan karena kebijakan lingkungan, melainkan karena force majeure. Rebound cepat setelah pembatasan dicabut membuktikan bahwa tanpa transformasi sistem transportasi dan energi, kualitas udara Jakarta tidak akan membaik secara berkelanjutan. Selama satu dekade penuh (2015–2024), tidak ada satu tahun pun — di luar anomali 2020 — yang rata-rata ISPU-nya turun mendekati zona BAIK (≤50), menandakan bahwa krisis kualitas udara Jakarta adalah persoalan struktural yang berulang, bukan fluktuasi acak.
ranking_stasiun <- data_clean %>%
group_by(stasiun) %>%
summarise(
rata_ispu = mean(max, na.rm = TRUE),
persen_buruk = mean(categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"),
na.rm = TRUE) * 100,
ispu_maks = max(max, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(rata_ispu))
stasiun_terburuk2 <- ranking_stasiun$stasiun[2] # peringkat ke-2 terburuk
ispu_st_terburuk2 <- round(ranking_stasiun$rata_ispu[2], 1)
heatmap_data <- data_clean %>%
group_by(tahun, stasiun) %>%
summarise(rata_ispu = mean(max, na.rm = TRUE), .groups = "drop")p_stasiun <- ggplot(ranking_stasiun,
aes(reorder(stasiun, rata_ispu), rata_ispu)) +
geom_col(aes(fill = rata_ispu), width = 0.7, show.legend = FALSE) +
geom_text(aes(label = paste0(round(rata_ispu, 1),
" | maks: ", round(ispu_maks, 0))),
hjust = -0.08, fontface = "bold", size = 4) +
scale_fill_gradient(low = "#f9e79f", high = "#c0392b") +
coord_flip(ylim = c(0, 115)) +
labs(x = NULL, y = "Rata-rata ISPU",
title = "Rata-rata ISPU per Stasiun (2015–2024)",
subtitle = paste0(stasiun_terburuk, " secara konsisten mencatat rata-rata ISPU tertinggi")) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.y = element_blank())
p_heatmap <- ggplot(heatmap_data,
aes(factor(tahun), reorder(stasiun, rata_ispu),
fill = rata_ispu)) +
geom_tile(color = "white", linewidth = 0.5) +
geom_text(aes(label = round(rata_ispu, 0)),
size = 2.8, color = "white", fontface = "bold") +
scale_fill_gradient2(low = "#27ae60", mid = "#f39c12", high = "#c0392b",
midpoint = 73, name = "ISPU") +
labs(x = "Tahun", y = NULL,
title = "Heatmap ISPU per Stasiun per Tahun",
subtitle = "Warna merah = ISPU tinggi; pola intensitas berbeda di tiap stasiun dapat ditelusuri per tahun") +
theme_minimal(base_size = 11) +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
panel.grid = element_blank())
p_stasiun / p_heatmap + plot_layout(heights = c(1, 1.3))Interpretasi: Terdapat kesenjangan kualitas udara yang signifikan antar wilayah Jakarta:
Lubang Buaya mencatat rata-rata ISPU tertinggi (76.4), diikuti oleh Kebon Jeruk (76.1). Pola ini umumnya berkaitan dengan kepadatan arus kendaraan, kedekatan dengan kawasan industri atau permukiman padat, serta sirkulasi udara yang lebih terbatas dibanding wilayah dengan ruang terbuka yang lebih luas.
Bundaran HI justru mencatat rata-rata ISPU terendah (62.5) meski terletak di kawasan padat aktivitas. Hal ini kemungkinan besar disebabkan oleh efek dispersi angin yang lebih efektif di area terbuka, lebar jalan yang memadai, dan keberadaan ruang terbuka hijau di sekitarnya.
Heatmap mengungkap pola temporal yang dapat ditelusuri per stasiun dan per tahun: intensitas warna merah tidak seragam dari tahun ke tahun, menandakan bahwa penyebab pencemaran di tiap wilayah memiliki dinamika dan sensitivitas musiman yang berbeda-beda, bukan semata mengikuti tren kota secara keseluruhan.
Implikasi Kebijakan: Strategi pengendalian pencemaran udara tidak bisa seragam untuk seluruh Jakarta. Lubang Buaya dan Kebon Jeruk membutuhkan intervensi lebih agresif, seperti pembatasan kendaraan berat, penghijauan koridor jalan, dan pemantauan real-time yang lebih ketat.
ggplot(data_clean, aes(reorder(stasiun, max, FUN = median), max, fill = stasiun)) +
geom_boxplot(show.legend = FALSE, outlier.alpha = 0.3,
outlier.size = 1.2, width = 0.6) +
geom_hline(yintercept = 100, linetype = "dashed",
color = "#e74c3c", linewidth = 0.8) +
geom_hline(yintercept = 50, linetype = "dashed",
color = "#27ae60", linewidth = 0.8) +
annotate("text", x = 0.7, y = 102,
label = "Batas TIDAK SEHAT", color = "#e74c3c",
size = 3, hjust = 0, fontface = "italic") +
annotate("text", x = 0.7, y = 52,
label = "Batas BAIK", color = "#27ae60",
size = 3, hjust = 0, fontface = "italic") +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Distribusi ISPU per Stasiun (2015–2024)",
subtitle = "Boxplot memperlihatkan sebaran, median, dan nilai ekstrem tiap stasiun",
x = NULL,
y = "Nilai ISPU Harian (Maks)"
) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.x = element_blank())Interpretasi: Boxplot memperjelas tidak hanya rata-rata, tetapi juga variabilitas dan nilai ekstrem tiap stasiun.
Lubang Buaya mencatat median ISPU tertinggi (74.5) sekaligus nilai ekstrem tertinggi yang pernah terukur di antara kelima stasiun (ISPU 287). Kombinasi ini menandakan stasiun tersebut bukan hanya secara konsisten berada di level lebih tinggi pada hari biasa, tetapi juga paling rentan terhadap episoda pencemaran ekstrem sesekali — menjadikannya prioritas utama untuk intervensi struktural sekaligus sistem peringatan dini.
Nilai ekstrem sesekali tetap berdampak kesehatan serius, bahkan jika rata-rata harian terlihat relatif aman.
# Normalisasi nama polutan (PM2,5 dari data 2022 → PM25)
polutan_clean <- data_clean %>%
filter(!is.na(critical)) %>%
mutate(critical = recode(critical, "PM2,5" = "PM25"))
polutan_summary <- polutan_clean %>%
count(critical) %>%
mutate(persen = n / sum(n) * 100) %>%
arrange(desc(persen))
tren_polutan <- polutan_clean %>%
count(tahun, critical) %>%
group_by(tahun) %>%
mutate(persen = n / sum(n) * 100) %>%
ungroup()
pol_dom_name <- polutan_summary$critical[1]
pol_dom_persen <- round(polutan_summary$persen[1], 1)p_polutan <- ggplot(polutan_summary,
aes(reorder(critical, persen), persen, fill = critical)) +
geom_col(show.legend = FALSE, width = 0.7) +
geom_text(aes(label = paste0(round(persen, 1), "%")),
hjust = -0.1, fontface = "bold", size = 4) +
scale_fill_manual(values = warna_polutan) +
coord_flip(ylim = c(0, max(polutan_summary$persen) + 8)) +
labs(x = NULL, y = "% Hari sebagai Polutan Dominan",
title = "Polutan Dominan Penyebab ISPU Tertinggi (2015–2024)",
subtitle = paste0(pol_dom_name, " menjadi polutan dominan terbanyak (",
pol_dom_persen, "% hari)")) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.y = element_blank())
p_area <- ggplot(tren_polutan, aes(tahun, persen, fill = critical)) +
geom_area(alpha = 0.85, position = "fill") +
scale_fill_manual(values = warna_polutan, name = "Polutan") +
scale_x_continuous(breaks = seq(2015, 2024, 1)) +
scale_y_continuous(labels = percent_format()) +
labs(x = "Tahun", y = "Proporsi Hari (%)",
title = "Pergeseran Komposisi Polutan Dominan per Tahun",
subtitle = "Pergeseran dari CO/PM10 menuju O3 dan PM2.5 mencerminkan perubahan pola emisi") +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom",
panel.grid.minor = element_blank())
p_polutan / p_area + plot_layout(heights = c(1, 1.3))Interpretasi: Ozon (O₃) mendominasi sebagai polutan penyebab ISPU tertinggi (38.2% hari). Ini adalah temuan krusial: O₃ bukan polutan yang langsung diemisikan (polutan primer), melainkan polutan sekunder yang terbentuk melalui reaksi fotokimia antara NOₓ dan Volatile Organic Compounds (VOC) di bawah sinar matahari.
Dominansi O₃ di Jakarta mengindikasikan bahwa: 1. Emisi NOₓ dari kendaraan bermotor dan industri sangat tinggi sebagai bahan baku pembentuk ozon. 2. Intensitas sinar matahari Jakarta yang tinggi sepanjang tahun mempercepat reaksi fotokimia. 3. Pengendalian harus difokuskan pada pengurangan emisi NOₓ dan VOC, bukan hanya partikel.
Area chart menunjukkan bahwa O₃ sudah menjadi polutan dominan sejak awal periode 2015–2024 (bukan baru muncul belakangan) — pada 2015–2017 saja O₃ sudah tercatat sebagai penyebab ISPU tertinggi di sekitar separuh hari, mengungguli PM10. Pergeseran yang lebih mencolok justru terlihat menjelang 2022–2024, ketika PM2.5 melonjak menjadi polutan dominan di sebagian besar hari. Pergeseran ini perlu dibaca hati-hati: seperti dicatat pada bagian “Fokus PM2.5” di bawah, parameter PM2.5 baru dipantau secara konsisten di seluruh stasiun sejak 2021. Artinya, sebagian dari lonjakan dominansi PM2.5 di akhir periode kemungkinan mencerminkan bertambahnya cakupan sensor, bukan murni perubahan komposisi polutan di udara. Yang tetap dapat disimpulkan dengan yakin adalah: karakter pencemaran Jakarta di dekade ini didominasi polutan sekunder/halus (O₃ dan PM2.5), bukan polutan kasar (PM10) atau gas primer (CO), sehingga strategi pengendalian perlu menyasar prekursor fotokimia (NOₓ, VOC) dan partikel halus sekaligus.
Perhatian Khusus PM2.5: Meski PM2.5 “hanya” berada di posisi ketiga, partikel berdiameter <2,5 mikron ini jauh lebih berbahaya karena mampu menembus jauh ke dalam alveolus paru-paru dan masuk ke aliran darah, berkaitan dengan penyakit kardiovaskular dan kanker paru.
pm25_tren <- data_clean %>%
filter(!is.na(pm25), pm25 > 0) %>%
group_by(tahun, stasiun) %>%
summarise(rata_pm25 = mean(pm25, na.rm = TRUE), .groups = "drop")
ggplot(pm25_tren, aes(factor(tahun), rata_pm25,
color = stasiun, group = stasiun)) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 100, ymax = Inf,
fill = "#e74c3c", alpha = 0.07) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 50, ymax = 100,
fill = "#f39c12", alpha = 0.07) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = 50,
fill = "#27ae60", alpha = 0.07) +
geom_hline(yintercept = 100, linetype = "dashed",
color = "#e74c3c", linewidth = 0.7) +
geom_hline(yintercept = 50, linetype = "dashed",
color = "#27ae60", linewidth = 0.7) +
geom_line(linewidth = 1.1) +
geom_point(size = 3) +
scale_color_brewer(palette = "Dark2", name = "Stasiun") +
labs(x = "Tahun", y = "Rata-rata Sub-Indeks ISPU PM2.5",
title = "Tren Sub-Indeks ISPU PM2.5 per Stasiun (2021-2024)",
subtitle = "Garis hijau = batas BAIK (50) | Garis merah = batas TIDAK SEHAT (100)",
caption = "Catatan: data PM2.5 baru dipantau konsisten sejak 2021; nilai adalah sub-indeks ISPU, bukan konsentrasi µg/m3") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")pm25_data <- data_clean %>% filter(!is.na(pm25), pm25 > 0)
pct_baik <- mean(pm25_data$pm25 <= 50, na.rm = TRUE) * 100
pct_sedang <- mean(pm25_data$pm25 > 50 & pm25_data$pm25 <= 100, na.rm = TRUE) * 100
pct_buruk <- mean(pm25_data$pm25 > 100, na.rm = TRUE) * 100
pm25_dom <- mean(pm25_data$critical == "PM25", na.rm = TRUE) * 100
tibble(
`Kategori Sub-Indeks PM2.5` = c("BAIK (≤50)", "SEDANG (51-100)", "TIDAK SEHAT ke atas (>100)"),
`% Hari` = c(paste0(round(pct_baik, 1), "%"),
paste0(round(pct_sedang, 1), "%"),
paste0(round(pct_buruk, 1), "%"))
) %>%
kable(caption = "Distribusi Sub-Indeks ISPU PM2.5 (2021-2024)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
full_width = FALSE) %>%
row_spec(3, bold = TRUE, color = "white", background = "#e67e22")| Kategori Sub-Indeks PM2.5 | % Hari |
|---|---|
| BAIK (≤50) | 12.1% |
| SEDANG (51-100) | 73.5% |
| TIDAK SEHAT ke atas (>100) | 14.4% |
Catatan satuan data: Nilai pm25 (dan polutan lain) pada dataset ini adalah sub-indeks ISPU hasil konversi konsentrasi ke skala 0-500 sesuai pedoman ISPU KLHK, bukan konsentrasi mentah dalam µg/m³. Karena itu nilai sub-indeks dibandingkan terhadap ambang batas kategori ISPU (50 dan 100), bukan terhadap baku mutu konsentrasi WHO atau PP No. 22 Tahun 2021.
Interpretasi: Data PM2.5 hanya tersedia secara konsisten sejak 2021 (parameter ini baru dipantau lebih luas di stasiun-stasiun SPKU pada periode tersebut), sehingga grafik di atas hanya mencerminkan kondisi 2021–2024. Pada periode ini, sub-indeks PM2.5 berada di 89.1% hari sebagai polutan kritis (penyumbang ISPU tertinggi) — menjadikannya kontributor penting meski bukan yang paling dominan dibanding O₃.
Tabel distribusi menunjukkan 14.4% hari sub-indeks PM2.5 saja sudah berada di kategori TIDAK SEHAT ke atas, sementara hanya 12.1% hari berada di kategori BAIK. Ini menegaskan bahwa PM2.5 tetap menjadi pencemar serius di Jakarta meski O₃ lebih sering menjadi penyebab nilai ISPU harian tertinggi.
Perlu dicatat bahwa keterbatasan data PM2.5 sebelum 2021 (banyak nilai NA) kemungkinan besar mencerminkan keterbatasan jangkauan alat pemantauan saat itu, bukan absennya polutan. Investasi pada sensor PM2.5 yang lebih luas dan konsisten sejak awal periode akan memberikan gambaran risiko jangka panjang yang lebih akurat.
bulanan <- data_clean %>%
group_by(bulan_n) %>%
summarise(
rata_ispu = mean(max, na.rm = TRUE),
median_ispu = median(max, na.rm = TRUE),
persen_buruk = mean(categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"),
na.rm = TRUE) * 100,
.groups = "drop"
) %>%
mutate(bulan = month(bulan_n, label = TRUE, abbr = FALSE))
musim_summary <- data_clean %>%
group_by(musim) %>%
summarise(
rata_ispu = mean(max, na.rm = TRUE),
persen_buruk = mean(categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"),
na.rm = TRUE) * 100,
jumlah_hari = n(),
.groups = "drop"
)
ispu_kemarau <- filter(musim_summary, grepl("Kemarau", musim))$rata_ispu
ispu_hujan <- filter(musim_summary, grepl("Hujan", musim))$rata_ispu
selisih_musim <- round(ispu_kemarau - ispu_hujan, 1)nama_bulan_id <- c("Jan","Feb","Mar","Apr","Mei","Jun",
"Jul","Agt","Sep","Okt","Nov","Des")
ggplot(bulanan, aes(bulan_n, rata_ispu, group = 1)) +
# Latar musim kemarau
annotate("rect", xmin = 4.5, xmax = 10.5, ymin = -Inf, ymax = Inf,
fill = "#f39c12", alpha = 0.12) +
# Label musim
annotate("text", x = 7.5, y = max(bulanan$rata_ispu) + 3,
label = "MUSIM KEMARAU (Mei–Okt)", fontface = "bold",
size = 3.8, color = "#e67e22") +
annotate("text", x = 2.3, y = max(bulanan$rata_ispu) + 3,
label = "MUSIM HUJAN", fontface = "bold",
size = 3.5, color = "#2980b9") +
annotate("text", x = 11.7, y = max(bulanan$rata_ispu) + 3,
label = "HUJAN", fontface = "bold",
size = 3.2, color = "#2980b9") +
# Ribbon selisih mean–median
geom_ribbon(aes(ymin = median_ispu, ymax = rata_ispu),
fill = "#e74c3c", alpha = 0.18) +
geom_line(color = "#c0392b", linewidth = 1.5) +
# Titik: ukuran = % hari tidak sehat
geom_point(aes(size = persen_buruk), color = "#c0392b", show.legend = FALSE) +
geom_text(aes(label = round(rata_ispu, 1)), vjust = -1.6, size = 3.5, fontface = "bold") +
scale_x_continuous(breaks = 1:12, labels = nama_bulan_id) +
scale_y_continuous(limits = c(45, max(bulanan$rata_ispu) + 6)) +
scale_size(range = c(3, 10)) +
labs(x = NULL, y = "Rata-rata ISPU",
title = "Pola ISPU Bulanan (2015–2024)",
subtitle = "Ukuran titik = % hari tidak sehat | Warna area = selisih rata-rata & median",
caption = "Sumber: DLH DKI Jakarta") +
theme_minimal(base_size = 13) +
theme(panel.grid.minor = element_blank())Interpretasi: Pola musiman ISPU Jakarta sangat jelas dan dapat diprediksi. Bulan August tercatat sebagai bulan dengan rata-rata ISPU tertinggi, sementara Januari adalah yang terendah. Terdapat selisih rata-rata sekitar 18 poin ISPU antara musim kemarau dan musim hujan.
Mekanisme fisik di balik pola ini: - Musim kemarau (Mei–Oktober): Tidak ada hujan yang berfungsi sebagai “pembersih” alami polutan di udara (wet deposition). Udara panas dan kering mempercepat reaksi fotokimia pembentukan O₃. Angin muson timur membawa partikel dari wilayah timur Indonesia. - Musim hujan (November–April): Hujan secara aktif mencuci partikel dan polutan dari atmosfer (washout effect), mengurangi konsentrasi PM10 dan PM2.5 secara signifikan.
Ukuran titik yang membesar di musim kemarau mengkonfirmasi bahwa risiko kesehatan meningkat di bulan-bulan kering. Informasi ini dapat dimanfaatkan untuk sistem peringatan dini berbasis kalender musim.
Peluang Intervensi: Pola musiman yang dapat diprediksi ini memberikan window of opportunity untuk kebijakan proaktif: pengetatan emisi kendaraan dan industri mulai April–Mei sebelum musim kemarau, penambahan penyiraman jalan di musim kemarau, dan kampanye kesehatan masyarakat yang terjadwal.
covid_years <- data_clean %>%
filter(tahun %in% c(2018, 2019, 2020, 2021)) %>%
group_by(tahun) %>%
summarise(
rata_ispu = mean(max, na.rm = TRUE),
persen_buruk = mean(categori %in%
c("TIDAK SEHAT", "SANGAT TIDAK SEHAT", "BERBAHAYA"),
na.rm = TRUE) * 100,
persen_baik = mean(categori == "BAIK", na.rm = TRUE) * 100,
.groups = "drop"
)
ispu_2018 <- filter(covid_years, tahun == 2018)$rata_ispu
ispu_2019 <- filter(covid_years, tahun == 2019)$rata_ispu
ispu_2020 <- filter(covid_years, tahun == 2020)$rata_ispu
ispu_2021 <- filter(covid_years, tahun == 2021)$rata_ispu
penurunan_pct <- (ispu_2019 - ispu_2020) / ispu_2019 * 100
rebound_pct <- (ispu_2021 - ispu_2020) / ispu_2020 * 100p_covid_bar <- ggplot(covid_years, aes(factor(tahun), rata_ispu, fill = factor(tahun))) +
geom_col(show.legend = FALSE, width = 0.65) +
geom_text(aes(label = round(rata_ispu, 1)),
vjust = -0.5, fontface = "bold", size = 5.5) +
# Panah & label penurunan 2019→2020
annotate("segment",
x = 2.1, xend = 2.9,
y = ispu_2019 + 1.5, yend = ispu_2020 + 1.5,
arrow = arrow(ends = "last", type = "closed",
length = unit(0.25, "cm")),
color = "#27ae60", linewidth = 1.5) +
annotate("text", x = 2.5, y = (ispu_2019 + ispu_2020) / 2 + 6,
label = paste0("↓ ", round(penurunan_pct, 0), "%\nPSBB"),
color = "#27ae60", fontface = "bold", size = 4) +
# Panah & label kenaikan 2020→2021
annotate("segment",
x = 3.1, xend = 3.9,
y = ispu_2020 + 1.5, yend = ispu_2021 + 1.5,
arrow = arrow(ends = "last", type = "closed",
length = unit(0.25, "cm")),
color = "#e74c3c", linewidth = 1.5) +
annotate("text", x = 3.5, y = (ispu_2020 + ispu_2021) / 2 + 6,
label = paste0("↑ ", round(rebound_pct, 0), "%\nRebound"),
color = "#e74c3c", fontface = "bold", size = 4) +
scale_fill_manual(values = c("2018" = "#e67e22", "2019" = "#c0392b",
"2020" = "#27ae60", "2021" = "#f39c12")) +
scale_y_continuous(limits = c(0, max(covid_years$rata_ispu) + 12)) +
labs(x = "Tahun", y = "Rata-rata ISPU",
title = "Dampak COVID-19 pada ISPU Jakarta (2018–2021)",
subtitle = paste0("Penurunan ", round(penurunan_pct, 0),
"% saat PSBB 2020 — rebound cepat sebesar ",
round(rebound_pct, 0), "% di 2021"),
caption = "Sumber: DLH DKI Jakarta") +
theme_minimal(base_size = 13) +
theme(panel.grid.minor = element_blank())
# Perbandingan bulanan 2019 vs 2020
covid_bulanan <- data_clean %>%
filter(tahun %in% c(2019, 2020)) %>%
group_by(tahun, bulan, bulan_n) %>%
summarise(rata_ispu = mean(max, na.rm = TRUE), .groups = "drop")
covid_wide <- covid_bulanan %>%
pivot_wider(names_from = tahun, values_from = rata_ispu, names_prefix = "ispu_") %>%
mutate(beda = ispu_2019 - ispu_2020)
p_covid_bulan <- ggplot(covid_bulanan,
aes(bulan, rata_ispu, color = factor(tahun), group = tahun)) +
geom_ribbon(data = covid_wide,
aes(x = bulan,
ymin = pmin(ispu_2019, ispu_2020),
ymax = pmax(ispu_2019, ispu_2020),
group = 1),
inherit.aes = FALSE, fill = "#27ae60", alpha = 0.22) +
geom_line(linewidth = 1.3) +
geom_point(size = 3.5) +
geom_text(data = filter(covid_wide, bulan_n %in% c(3, 5, 7, 9)),
aes(x = bulan,
y = (ispu_2019 + ispu_2020) / 2,
label = paste0("-", round(beda, 0), " poin")),
inherit.aes = FALSE, color = "#27ae60",
fontface = "bold", size = 3.5, vjust = -0.9) +
scale_color_manual(values = c("2019" = "#c0392b", "2020" = "#27ae60"),
name = "Tahun") +
labs(x = NULL, y = "Rata-rata ISPU",
title = "ISPU Bulanan: 2019 vs 2020",
subtitle = "Area hijau = selisih penurunan ISPU akibat PSBB") +
theme_minimal(base_size = 13) +
theme(legend.position = "bottom")
p_covid_bar / p_covid_bulanInterpretasi: Analisis periode COVID-19 memberikan bukti kausalitas terkuat yang pernah ada untuk hubungan antara aktivitas manusia dan kualitas udara Jakarta:
covid_kat <- data_clean %>%
filter(tahun %in% c(2019, 2020)) %>%
count(tahun, categori) %>%
group_by(tahun) %>%
mutate(persen = n / sum(n) * 100,
categori = factor(categori, levels = kategori_order)) %>%
filter(!is.na(categori))
ggplot(covid_kat, aes(factor(tahun), persen, fill = categori)) +
geom_col(position = "stack", width = 0.5) +
scale_fill_manual(values = warna_kategori, name = "Kategori ISPU") +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
geom_text(aes(label = ifelse(persen > 3, paste0(round(persen, 0), "%"), "")),
position = position_stack(vjust = 0.5),
color = "white", fontface = "bold", size = 4) +
labs(x = NULL, y = "Persentase Hari (%)",
title = "Distribusi Kategori ISPU: 2019 vs 2020",
subtitle = "Proporsi hari BAIK meningkat & hari TIDAK SEHAT berkurang signifikan di 2020") +
theme_minimal(base_size = 13)Interpretasi: Stacked bar chart mempertegas dampak PSBB dari perspektif distribusi kategori. Di tahun 2020, proporsi hari dengan udara BAIK meningkat dan hari TIDAK SEHAT berkurang dibandingkan 2019. Ini mengkonfirmasi bahwa bukan hanya rata-rata ISPU yang turun, tetapi secara nyata lebih banyak hari yang dapat dinikmati warga Jakarta dengan udara yang lebih sehat. Temuan ini seharusnya menjadi landasan kebijakan: investasi pada transportasi publik, elektrifikasi kendaraan, dan pengurangan emisi industri dapat memberikan dampak yang sebanding dengan PSBB, tetapi secara permanen dan tanpa mengorbankan perekonomian.
data_clean %>%
group_by(tahun, stasiun) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop") %>%
ggplot(aes(tahun, rata, color = stasiun, group = stasiun)) +
geom_line(linewidth = 1.1) +
geom_point(size = 2.5) +
geom_hline(yintercept = 100, linetype = "dashed",
color = "#e74c3c", linewidth = 0.6, alpha = 0.7) +
geom_hline(yintercept = 50, linetype = "dashed",
color = "#27ae60", linewidth = 0.6, alpha = 0.7) +
facet_wrap(~stasiun, ncol = 3) +
scale_x_continuous(breaks = seq(2015, 2024, 2)) +
scale_color_brewer(palette = "Dark2") +
labs(
title = "Tren ISPU Tahunan per Stasiun (2015–2024)",
subtitle = "Garis merah putus = batas TIDAK SEHAT (100) | Garis hijau putus = batas BAIK (50)",
x = "Tahun",
y = "Rata-rata ISPU",
caption = "Sumber: DLH DKI Jakarta"
) +
theme_minimal(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold", size = 10),
axis.text.x = element_text(angle = 45, hjust = 1, size = 8)
)Interpretasi: Visualisasi faceted memungkinkan perbandingan tren antar stasiun secara bersamaan. Beberapa pola menonjol:
Versi gabungan (bukan faceted) berikut menumpuk kelima stasiun dalam satu panel dengan zona warna latar belakang, agar pembaca infografis bisa langsung melihat posisi tiap stasiun relatif terhadap ambang BAIK dan TIDAK SEHAT sekaligus membandingkannya satu sama lain.
tren_stasiun_gab <- data_clean %>%
group_by(tahun, stasiun) %>%
summarise(rata = mean(max, na.rm = TRUE), .groups = "drop")
warna_stasiun_gab <- c(
"Lubang Buaya" = "#e63946",
"Jagakarsa" = "#b5651d",
"Kebon Jeruk" = "#f4a300",
"Kelapa Gading" = "#f7c948",
"Bundaran HI" = "#2b7cd3"
)
batas_atas_y <- ceiling(max(tren_stasiun_gab$rata) / 10) * 10 + 20
ggplot(tren_stasiun_gab, aes(x = tahun, y = rata, color = stasiun, group = stasiun)) +
# Zona latar belakang: merah muda (>100, TIDAK SEHAT), hijau muda (<=50, BAIK)
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 100, ymax = batas_atas_y,
fill = "#e74c3c", alpha = 0.10) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 0, ymax = 50,
fill = "#27ae60", alpha = 0.12) +
geom_hline(yintercept = 100, color = "#e74c3c", linewidth = 0.5, alpha = 0.6) +
geom_hline(yintercept = 50, color = "#27ae60", linewidth = 0.5, alpha = 0.6) +
geom_line(linewidth = 1, alpha = 0.9) +
geom_point(size = 2.6) +
scale_color_manual(values = warna_stasiun_gab, name = NULL) +
scale_x_continuous(breaks = seq(2015, 2024, 1)) +
scale_y_continuous(limits = c(0, batas_atas_y), breaks = seq(0, batas_atas_y, 50),
expand = expansion(mult = c(0, 0))) +
labs(title = "Tren ISPU per Stasiun (2015–2024)", x = NULL, y = NULL) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
legend.position = "bottom",
legend.text = element_text(size = 10)
) +
guides(color = guide_legend(nrow = 2, byrow = TRUE))Interpretasi: Kelima stasiun bergerak dengan pola musiman yang serempak — naik di 2018–2019, jatuh bersama di 2020 (PSBB), lalu naik lagi di 2021–2022 — mengonfirmasi bahwa pencemaran udara Jakarta dominan dipengaruhi faktor kota-wide (mobilitas dan cuaca regional), bukan semata kondisi lokal tiap wilayah. Namun ada jarak (gap) yang konsisten antar stasiun: Lubang Buaya dan Kebon Jeruk secara sistematis berada di pita atas mendekati atau melewati zona TIDAK SEHAT, sementara Bundaran HI konsisten di pita bawah, kadang mendekati zona BAIK. Jarak yang stabil ini menunjukkan perbedaan karakteristik struktural antar wilayah (kepadatan lalu lintas, tata ruang, tutupan vegetasi) yang tidak hilang meski tren tahunan naik-turun bersamaan.
cor_polutan <- data_clean %>%
select(pm25, pm10, o3, no2, so2, co) %>%
cor(use = "pairwise.complete.obs")
corrplot(cor_polutan,
method = "color",
type = "upper",
tl.col = "black",
tl.srt = 45,
addCoef.col = "white",
number.cex = 0.85,
col = colorRampPalette(c("#3498db", "white", "#e74c3c"))(200),
title = "Matriks Korelasi Antar Polutan Udara (2015–2024)",
mar = c(0, 0, 2, 0))Catatan Metodologi: Korelasi yang melibatkan
PM2.5 dihitung dari jumlah observasi yang jauh lebih
sedikit (data tersedia konsisten sejak 2021) dibanding korelasi antar
polutan lain yang memakai data penuh 2015–2024. Ini wajar secara
statistik (pairwise.complete.obs), tetapi berarti angka
korelasi PM2.5 secara implisit hanya mencerminkan kondisi 2021–2024,
bukan seluruh dekade.
Interpretasi: Matriks korelasi mengungkap hubungan struktural antar polutan yang penting untuk memahami sumber emisi dan proses atmosferik:
PM2.5 & PM10 berkorelasi kuat positif (r ≈ 0,8): Keduanya berbagi sumber yang sama — emisi kendaraan bermotor, debu jalanan, dan partikel konstruksi. Pengendalian satu polutan secara otomatis akan mereduksi yang lain.
CO berkorelasi moderat dengan PM2.5, tetapi lemah dengan PM10: Karbon monoksida (CO) yang berasal dari pembakaran tidak sempurna kendaraan bermotor tampak lebih terkait dengan partikel halus (PM2.5) dibanding partikel kasar (PM10) — mengindikasikan bahwa PM10 di Jakarta juga banyak berasal dari sumber non-pembakaran langsung (debu jalan, konstruksi), bukan semata emisi kendaraan.
O₃ memiliki pola korelasi berbeda dari gas hasil pembakaran langsung (NO₂, SO₂): O₃ berkorelasi negatif moderat dengan NO₂ dan SO₂ — konsisten dengan sifatnya sebagai polutan sekunder yang justru terbentuk dari reaksi NOₓ dan VOC di bawah sinar matahari, dan dapat terdegradasi oleh NO segar dari knalpot kendaraan (efek titrasi). Menariknya, O₃ justru berkorelasi lemah positif dengan PM dan CO — kemungkinan karena hari-hari cerah dengan sinar matahari kuat (pemicu O₃) juga cenderung menjadi hari dengan aktivitas kota yang tinggi, sehingga emisi primer ikut naik bersamaan.
SO₂ & NO₂ berkorelasi moderat positif (r ≈ 0,4): Mencerminkan emisi dari pembakaran bahan bakar fosil yang sama (terutama industri dan pembangkit listrik).
Implikasi Analisis: Temuan korelasi ini menyarankan dua strategi berbeda: (1) Pengendalian PM2.5, PM10, dan CO dapat dilakukan bersamaan melalui pengendalian emisi kendaraan, debu jalan, dan konstruksi; (2) Pengendalian O₃ memerlukan pendekatan berbeda — fokus pada prekursor NOₓ dan VOC, bukan sekadar menekan polutan partikel atau CO secara langsung.
top15 <- data_clean %>%
arrange(desc(max)) %>%
head(15)
top15 %>%
select(Tanggal = tanggal,
Stasiun = stasiun,
`ISPU Maks` = max,
Polutan = critical,
Kategori = categori) %>%
kable(caption = "15 Hari dengan ISPU Tertinggi yang Tercatat (2015-2024)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "bordered")) %>%
row_spec(which(top15$max >= 150),
bold = TRUE, color = "white", background = "#c0392b")| Tanggal | Stasiun | ISPU Maks | Polutan | Kategori |
|---|---|---|---|---|
| 2023-09-28 | Lubang Buaya | 287 | PM25 | SANGAT TIDAK SEHAT |
| 2023-09-26 | Lubang Buaya | 249 | PM25 | SANGAT TIDAK SEHAT |
| 2019-04-07 | Kebon Jeruk | 243 | O3 | SANGAT TIDAK SEHAT |
| 2019-04-08 | Kebon Jeruk | 235 | O3 | SANGAT TIDAK SEHAT |
| 2018-07-28 | Kebon Jeruk | 234 | O3 | SANGAT TIDAK SEHAT |
| 2018-11-16 | Kelapa Gading | 234 | O3 | SANGAT TIDAK SEHAT |
| 2018-09-14 | Kelapa Gading | 233 | O3 | SANGAT TIDAK SEHAT |
| 2018-07-16 | Kebon Jeruk | 222 | O3 | SANGAT TIDAK SEHAT |
| 2019-11-09 | Kebon Jeruk | 220 | O3 | SANGAT TIDAK SEHAT |
| 2018-09-04 | Kebon Jeruk | 217 | O3 | SANGAT TIDAK SEHAT |
| 2018-10-17 | Kebon Jeruk | 217 | O3 | SANGAT TIDAK SEHAT |
| 2023-09-27 | Lubang Buaya | 217 | PM25 | SANGAT TIDAK SEHAT |
| 2019-03-09 | Kebon Jeruk | 215 | O3 | SANGAT TIDAK SEHAT |
| 2018-12-12 | Kelapa Gading | 214 | O3 | SANGAT TIDAK SEHAT |
| 2018-06-29 | Kebon Jeruk | 213 | O3 | SANGAT TIDAK SEHAT |
Interpretasi: Tabel ini menampilkan episoda pencemaran terparah yang pernah tercatat selama 10 tahun. Rekor tertinggi terjadi pada 28 September 2023 di stasiun Lubang Buaya dengan ISPU mencapai 287 (kategori SANGAT TIDAK SEHAT), dipicu oleh polutan PM25.
Beberapa pola penting dari 15 episoda terburuk ini:
O3 mendominasi sebagai pemicu utama (12 dari 15 episoda terburuk), mengonfirmasi bahwa kondisi pencemaran paling ekstrem di Jakarta umumnya terjadi akibat reaksi fotokimia yang intens di bawah sinar matahari terik dan udara yang relatif minim hujan pembersih.
Kebon Jeruk menyumbang 9 dari 15 episoda terburuk, menandakan bahwa kondisi atmosferik dan geografis lokal stasiun ini (sirkulasi udara terbatas, kepadatan emisi) membuatnya rentan terhadap penumpukan polutan pada kondisi cuaca tertentu.
Sebanyak 0 dari 15 episoda terburuk mencapai kategori BERBAHAYA (ISPU ≥ 300) — kondisi darurat udara di mana seluruh populasi berisiko, bukan hanya kelompok sensitif. Episoda seperti ini, meski jarang, perlu menjadi pemicu protokol darurat kualitas udara (misalnya penutupan sekolah atau pembatasan aktivitas luar ruang).
ispu_2024_val <- filter(trend_tahun, tahun == 2024)$rata_ispu
skenario <- tibble(
skenario = c("Kondisi Aktual 2024",
"Skenario: Pembatasan Mobilitas\n(setara efek COVID-2020)",
"Skenario: Target Kualitas BAIK"),
rata_ispu = c(
ispu_2024_val,
ispu_2024_val * (1 - penurunan_pct / 100),
50
),
warna = c("#c0392b", "#f39c12", "#27ae60")
)
ggplot(skenario, aes(reorder(skenario, -rata_ispu), rata_ispu, fill = warna)) +
geom_col(width = 0.55, show.legend = FALSE) +
geom_text(aes(label = round(rata_ispu, 1)),
vjust = -0.5, fontface = "bold", size = 6) +
geom_hline(yintercept = 50, linetype = "dashed",
color = "#27ae60", linewidth = 1.1) +
geom_hline(yintercept = 100, linetype = "dashed",
color = "#e74c3c", linewidth = 0.8, alpha = 0.6) +
annotate("text", x = 0.55, y = 52,
label = "Batas Target BAIK (≤50)",
color = "#27ae60", fontface = "italic", size = 3.8, hjust = 0) +
scale_fill_identity() +
scale_y_continuous(limits = c(0, ispu_2024_val + 15)) +
labs(x = NULL, y = "Rata-rata ISPU",
title = "Simulasi Dampak Kebijakan terhadap ISPU Jakarta",
subtitle = paste0("Basis: kondisi 2024 (ISPU ", round(ispu_2024_val, 1),
") | Potensi penurunan ", round(penurunan_pct, 0),
"% dari pembatasan mobilitas serius")) +
theme_minimal(base_size = 13) +
theme(panel.grid.minor = element_blank(),
axis.text.x = element_text(size = 10))Interpretasi: Simulasi ini menggunakan besaran penurunan ISPU yang teramati saat PSBB 2020 sebagai proksi efek kebijakan pembatasan mobilitas. Hasilnya menunjukkan bahwa bahkan dengan skenario “pembatasan mobilitas setara PSBB”, ISPU Jakarta masih belum mencapai target BAIK (≤50) — menandakan bahwa pembatasan kendaraan saja tidak cukup.
Untuk mencapai kategori BAIK secara konsisten, Jakarta membutuhkan: 1. Transformasi sistem transportasi: Ekspansi masif transportasi publik, elektrifikasi kendaraan, dan pengendalian emisi ketat. 2. Penghijauan kota: Peningkatan ruang terbuka hijau sebagai filter alami polutan. 3. Transisi energi industri: Pengurangan emisi dari sektor non-transportasi (pembangkit listrik, industri, konstruksi). 4. Regulasi berkelanjutan: Standar emisi Euro 5/6 untuk kendaraan baru dan pensiun kendaraan tua berpolusi.
Catatan Metodologi: Simulasi ini bersifat ilustratif dan mengasumsikan hubungan linear antara pembatasan mobilitas dan penurunan ISPU. Kondisi aktual lebih kompleks karena melibatkan faktor meteorologis, komposisi armada kendaraan, dan sumber emisi non-kendaraan yang sulit dikontrol.
Dua grafik berikut dirancang khusus dengan gaya flat dan warna semantik (bukan gaya laporan analitis di atas) agar siap dipakai langsung sebagai aset visual infografis.
musim_infografis <- data_clean %>%
mutate(musim_label = ifelse(bulan_n %in% 5:10, "Kemarau", "Hujan")) %>%
group_by(musim_label) %>%
summarise(rata_ispu = round(mean(max, na.rm = TRUE), 1), .groups = "drop") %>%
mutate(musim_label = factor(musim_label, levels = c("Kemarau", "Hujan")))
warna_musim <- c("Kemarau" = "#EF9F27", "Hujan" = "#639922")
p_musim_info <- ggplot(musim_infografis, aes(x = musim_label, y = rata_ispu, fill = musim_label)) +
geom_col(width = 0.55, show.legend = FALSE) +
geom_text(aes(label = rata_ispu), vjust = -0.6, size = 6, fontface = "bold",
color = "#2C2C2A") +
scale_fill_manual(values = warna_musim) +
scale_y_continuous(limits = c(0, max(musim_infografis$rata_ispu) * 1.25),
expand = expansion(mult = c(0, 0))) +
labs(x = NULL, y = NULL, title = NULL) +
theme_minimal(base_size = 13) +
theme(
panel.grid = element_blank(),
axis.text.y = element_blank(),
axis.text.x = element_text(size = 13, face = "bold", color = "#2C2C2A"),
axis.ticks = element_blank(),
plot.margin = margin(10, 10, 10, 10)
)
p_musim_infoInterpretasi: musim kemarau (Mei–Oktober) rata-rata ISPU-nya jauh lebih tinggi dibanding musim hujan — selisih sekitar 18 poin, mengindikasikan kondisi atmosfer kering dan minim hujan mempercepat akumulasi polutan serta memicu pembentukan O₃ fotokimia.
polutan_infografis <- data_clean %>%
mutate(critical = case_when(
critical %in% c("PM2,5", "PM25", "PM 2.5") ~ "PM2.5",
TRUE ~ critical
)) %>%
count(critical) %>%
mutate(persen = round(n / sum(n) * 100, 1)) %>%
arrange(desc(persen)) %>%
mutate(critical = factor(critical, levels = critical))
warna_polutan <- c(
"O3" = "#EF9F27",
"PM2.5" = "#E24B4A",
"PM10" = "#888780",
"SO2" = "#B4B2A9",
"CO" = "#D3D1C7",
"NO2" = "#F1EFE8"
)
p_donut_info <- ggplot(polutan_infografis, aes(x = 2, y = persen, fill = critical)) +
geom_col(width = 1, color = "white", linewidth = 1.5) +
coord_polar(theta = "y") +
xlim(0.5, 2.5) +
scale_fill_manual(values = warna_polutan) +
theme_void(base_size = 13) +
theme(legend.position = "none") +
# Label persen untuk 3 polutan teratas saja (batasi noise visual)
geom_text(
data = filter(polutan_infografis, row_number() <= 3),
aes(label = paste0(critical, "\n", persen, "%"), x = 2.5),
position = position_stack(vjust = 0.5),
size = 4, fontface = "bold", color = "#2C2C2A"
)
p_donut_infoInterpretasi: O₃ (38.2%) dan PM2.5 (30.7%) bersama-sama menyumbang hampir 70% dari seluruh episoda ISPU tertinggi — keduanya polutan sekunder/halus yang lebih sulit dikendalikan hanya dengan mengurangi sumber emisi langsung, berbeda dari PM10 (24.7%) yang lebih terkait debu dan partikel kasar.
# Normalisasi penulisan PM2.5 yang tidak konsisten di data mentah
data_pm25norm <- data_clean %>%
mutate(critical = case_when(
critical %in% c("PM25", "PM2,5") ~ "PM2.5",
TRUE ~ critical
))
buat_ringkasan_polutan <- function(df, label_kategori_lain = "Lainnya") {
df %>%
count(critical) %>%
mutate(persen = n / sum(n) * 100) %>%
arrange(desc(persen)) %>%
mutate(
kelompok = ifelse(row_number() <= 3, critical, label_kategori_lain)
) %>%
group_by(kelompok) %>%
summarise(persen = sum(persen), .groups = "drop") %>%
arrange(desc(persen)) %>%
mutate(kelompok = factor(kelompok, levels = kelompok))
}
polutan_pra <- data_pm25norm %>% filter(tahun >= 2015, tahun <= 2020) %>% buat_ringkasan_polutan()
polutan_pasca <- data_pm25norm %>% filter(tahun >= 2021, tahun <= 2024) %>% buat_ringkasan_polutan()
persen_pm25_pasca <- round(polutan_pasca$persen[polutan_pasca$kelompok == "PM2.5"], 1)warna_banding <- c(
"O3" = "#6c5ce7", "PM2.5" = "#6c5ce7",
"PM10" = "#2980b9",
"CO" = "#e67e22", "SO2" = "#e67e22", "CO+SO2" = "#e67e22",
"Lainnya" = "#bdc3c7"
)
buat_donut <- function(df, judul) {
ggplot(df, aes(x = 2, y = persen, fill = kelompok)) +
geom_col(width = 1, color = "white", linewidth = 1.2) +
coord_polar(theta = "y") +
xlim(0.5, 2.5) +
scale_fill_manual(values = warna_banding) +
labs(title = judul) +
theme_void(base_size = 12) +
theme(
legend.position = "top",
legend.title = element_blank(),
legend.text = element_text(size = 10),
plot.title = element_text(size = 11, face = "bold", hjust = 0, margin = margin(b = 8))
) +
guides(fill = guide_legend(nrow = 1)) +
geom_text(aes(label = paste0(kelompok, " ", round(persen, 1), "%"), x = 2.5),
position = position_stack(vjust = 0.5), size = 3.3, color = "#2c3e50")
}
p_donut_pra <- buat_donut(polutan_pra, "Polutan kritis 2015–2020 (PM2.5 belum konsisten dipantau)")
p_donut_pasca <- buat_donut(polutan_pasca, "Polutan kritis 2021–2024 (PM2.5 mulai dipantau)")
p_donut_pra + p_donut_pascaO₃ dominan sebagian karena PM2.5 belum diukur konsisten — bukan karena PM2.5 tidak ada. Ini buta data, bukan buta polutan.
Begitu PM2.5 mulai diukur luas sejak 2021, ia langsung mendominasi 83.4% hari — polutan yang selama ini nyaris tidak terlihat di data ternyata yang paling sering jadi penyebab ISPU tertinggi.
Catatan Metodologi: Perbandingan ini memakai batas 2021 karena itu titik saat cakupan sensor PM2.5 meluas ke seluruh stasiun (lihat catatan keterbatasan data PM2.5 di bagian “Fokus PM2.5” sebelumnya). Perbedaan persentase antar periode karena itu mencerminkan gabungan dua hal: perluasan cakupan sensor DAN kemungkinan perubahan pola polusi aktual — keduanya tidak bisa dipisahkan sepenuhnya dari data ini.
1. Tidak ada perbaikan struktural dalam 10 tahun. Meski terjadi fluktuasi, rata-rata ISPU Jakarta tidak menunjukkan tren penurunan yang konsisten dari 2015 hingga 2024. Puncak tertinggi tercatat pada 2019 (rata-rata ISPU 80.9) — jauh setelah anomali penurunan akibat pandemi di 2020 — menandakan pola pencemaran yang berulang tanpa solusi permanen.
2. O₃ adalah polutan utama — bukan PM. Dominasi ozon menandakan perubahan karakter pencemaran udara Jakarta dari berbasis partikel ke berbasis kimia-fotokimia. Strategi pengendalian harus beradaptasi dengan tantangan ini.
3. COVID-19 membuktikan kausalitas. Penurunan 22% ISPU saat PSBB adalah bukti paling kuat bahwa aktivitas manusia — terutama transportasi — adalah penyebab dominan pencemaran udara Jakarta.
4. Musim kemarau adalah periode kritis. Perbedaan 18 poin ISPU antara musim kemarau dan hujan memberikan baseline untuk sistem peringatan dini dan intensifikasi kebijakan musiman.
5. Lubang Buaya membutuhkan perhatian khusus. Dengan rata-rata ISPU tertinggi (76.4), diikuti oleh Kebon Jeruk (76.1), kedua wilayah ini memerlukan intervensi yang lebih intensif dibanding kawasan lain.
| Prioritas | Rekomendasi | Dampak Perkiraan |
|---|---|---|
| Tinggi | Percepat elektrifikasi angkutan umum & kendaraan pribadi | Penurunan CO, PM, NOₓ signifikan |
| Tinggi | Terapkan standar emisi Euro 5/6 dan pensiun kendaraan berpolusi tinggi | Penurunan emisi partikel & SO₂ |
| Menengah | Perkuat sistem pemantauan real-time PM2.5 di seluruh stasiun SPKU | Data lebih akurat untuk kebijakan berbasis bukti |
| Menengah | Kampanye peringatan udara berbasis musim (intensif Mei–Oktober) | Pengurangan paparan kelompok sensitif |
| Jangka Panjang | Integrasi kebijakan tata ruang: perlindungan RTH dan sabuk hijau kota | Filter alami & penyerapan polutan jangka panjang |
Penutup: Data tidak berbohong. Selama 10 tahun, Jakarta telah mengumpulkan ribuan hari bukti bahwa udara yang dihirup warganya tidak memenuhi standar kesehatan. Pandemi 2020 memberi kita sebentar napas — dalam arti literal. Tantangannya kini adalah mentransfer “efek pandemi” ke dalam “efek kebijakan” yang permanen, tanpa harus menunggu bencana lain untuk membuktikan bahwa perubahan itu mungkin dan perlu.
Sumber Data: Dinas Lingkungan Hidup (DLH) DKI
Jakarta — Data ISPU dari 5 Stasiun Pemantauan Kualitas Udara (SPKU):
Bundaran HI, Kelapa Gading, Jagakarsa, Lubang Buaya, dan Kebon Jeruk.
Periode analisis: 1 Januari 2015 – 31 Desember 2024 (disaring dari file
gabungan data_ispu_dki_2010_2024.xlsx yang aslinya mencakup
2010–2024), total 16.543 hari observasi valid.
Analisis oleh: Fadly Syabani | Dihasilkan
pada: 08 September 2026