Bagian 1 menutup dengan berita yang sudah terpetakan: 8 hari NaN dan 3 nilai mustahil. Mengetahui ada masalah berbeda dengan mengetahui di mana dan kenapa — dan keduanya dibutuhkan sebelum satu angka pun boleh diubah. Pada bagian ini kita membangun dua fungsi yang bekerja sebagai pasangan: deteksiMasalah menandai titik bermasalah dengan dua lapis pemeriksaan yang saling mengoreksi, lalu bersihkanDeret memperbaikinya lewat interpolasi tanpa mengubah panjang deret. Ukuran keberhasilannya terukur: 730 hari masuk, 730 hari keluar, tanpa NaN, tanpa nilai tak fisik.
Yang kita bangun pada bagian ini:
deteksiMasalah— tiga mask: NaN, outlier domain fisik, outlier statistik z-score,bersihkanDeret— interpolasiinterp1linear dari tetangga sah, plus statistik sebelum/sesudah,- keputusan perbaikan — kapan nilai diganti, kapan dibuang, kapan dibiarkan.
Dua Lapis Pemeriksaan Outlier #
Satu alat deteksi tidak pernah cukup. Batas fisik menangkap kesalahan yang mustahil terjadi di alam, tetapi buta terhadap nilai yang salah namun masih masuk akal (misalnya 28 tercatat padahal seharusnya 21). Z-score menangkap nilai yang jauh dari kerumunan, tetapi bisa meloloskan 45 jika data lain ikut termakan rata-ratanya. Kita pakai keduanya:
| Aspek | Domain fisik | Statistik (z-score) |
|---|---|---|
| Pertanyaan | “Apakah nilai ini mungkin terjadi?” | “Apakah nilai ini jauh dari teman-temannya?” |
| Ambang | suhu < 10 atau suhu > 40 derajat | |z| > 3 |
| Kebutuhan data | pengetahuan domain (iklim Indonesia) | mean dan std data yang sah |
| Menangkap | -5, 60, 45 — salah ketik/sensor rusak | nilai ekstrem relatif apa pun |
| Kelemahan | nilai salah yang masih “wajar” | tertipu jika outlier menyesatkan mean/std |
Lapis statistik dihitung setelah NaN dikeluarkan — mean dan std biasa akan menyebarkan NaN ke seluruh hasil, sehingga suhu(sah) dengan mask sah = ~isnan(suhu) adalah prasyaratnya. Mari kita lihat z-score bekerja pada cuplikan 15 nilai:
v = [26.2; 27.1; 25.8; 28.4; 27.6; 26.9; 28.0; 27.3; 25.9; 26.6; 27.8; 26.4; 27.2; 25.5; 60.0];
z = (v - mean(v)) ./ std(v);
fprintf('suhu z-score\n');
for k = 1:numel(v)
fprintf('%5.1f %9.3f\n', v(k), z(k));
end
fprintf('mean = %.4f, std = %.4f, jumlah |z|>3: %d\n', mean(v), std(v), sum(abs(z) > 3));Output yang Dihasilkan:
suhu z-score
26.2 -0.339
27.1 -0.234
25.8 -0.386
28.4 -0.083
27.6 -0.176
26.9 -0.258
28.0 -0.130
27.3 -0.211
25.9 -0.374
26.6 -0.293
27.8 -0.153
26.4 -0.316
27.2 -0.223
25.5 -0.421
60.0 3.597
mean = 29.1133, std = 8.5864, jumlah |z|>3: 1Nilai 60.0 mencatat z-score 3.597 dan tertandai. Perhatikan juga efeknya: satu nilai liar mengangkat mean ke 29.11 dan std ke 8.59 — hampir seluruh nilai normal ikut “turun” z-nya. Inilah masking: pada cuplikan yang lebih pendek lagi, outlier bisa menutupi dirinya sendiri (maksimum z pada n data hanya sekitar sqrt(n)), itulah sebabnya deteksi kita diberi amunisi domain fisik juga.
deteksiMasalah: Tiga Mask dalam Satu Fungsi #
Berikut src/deteksiMasalah.m:
function m = deteksiMasalah(suhu)
% DETEKSIMASALAH Mendeteksi NaN dan outlier pada deret suhu.
% m = deteksiMasalah(suhu)
%
% Dua lapis pemeriksaan outlier:
% 1. Domain fisik: iklim Indonesia tidak masuk akal di luar 10..40 derajat.
% 2. Statistik : z-score > 3 terhadap mean/std data yang sah (tanpa NaN).
%
% Struktur keluaran (semua mask berukuran sama dengan suhu):
% m.maskNaN, m.posisiNaN, m.jumlahNaN
% m.maskDomain, m.posisiDomain, m.jumlahDomain
% m.maskStatistik, m.posisiStatistik, m.jumlahStatistik
% m.rataRataSah, m.deviasiSah
if nargin < 1
error('deteksiMasalah:input', 'Sediakan vektor suhu.');
end
suhu = suhu(:);
% ---- lapis 0: hari hilang ----
m.maskNaN = isnan(suhu);
m.posisiNaN = find(m.maskNaN);
m.jumlahNaN = numel(m.posisiNaN);
sah = ~m.maskNaN;
% ---- lapis 1: outlier domain fisik ----
batasBawah = 10; % derajat, sumbu dingin tak fisik utk iklim Indonesia
batasAtas = 40; % derajat, sumbu panas tak fisik utk iklim Indonesia
m.maskDomain = sah & (suhu < batasBawah | suhu > batasAtas);
m.posisiDomain = find(m.maskDomain);
m.jumlahDomain = numel(m.posisiDomain);
% ---- lapis 2: outlier statistik (z-score > 3) ----
m.rataRataSah = mean(suhu(sah));
m.deviasiSah = std(suhu(sah));
z = (suhu - m.rataRataSah) ./ m.deviasiSah;
m.maskStatistik = sah & (abs(z) > 3);
m.posisiStatistik = find(m.maskStatistik);
m.jumlahStatistik = numel(m.posisiStatistik);
% ---- ringkasan ----
fprintf('=== Deteksi Masalah Data ===\n');
fprintf('Hari hilang (NaN) : %d hari, posisi %s\n', ...
m.jumlahNaN, mat2str(m.posisiNaN(:)'));
fprintf('Outlier domain fisik : %d hari, posisi %s\n', ...
m.jumlahDomain, mat2str(m.posisiDomain(:)'));
fprintf('Outlier statistik (|z|>3) : %d hari, posisi %s\n', ...
m.jumlahStatistik, mat2str(m.posisiStatistik(:)'));
fprintf('Statistik data sah (n=%d) : mean %.4f, std %.4f\n', ...
sum(sah), m.rataRataSah, m.deviasiSah);
endMari kita bedah baris-baris kode di atas:
- Baris 18 (
suhu = suhu(:);): vektor dipaksa menjadi kolom. Semua mask yang dihasilkan akan berbentuk kolom 730x1 — sejajar dengandata.haridari Bagian 1, sehingga operasimask1 | mask2tidak pernah tersandung perbedaan orientasi. - Baris 21-23 (
m.maskNaN = isnan(suhu);dst.):isnanmenghasilkan mask logikal;findmengubahnya menjadi indeks posisi. Mask untuk mesin (logical indexing), posisi untuk manusia (laporan) — dua representasi data yang sama. - Baris 30 (
m.maskDomain = sah & (suhu < batasBawah | suhu > batasAtas);): perbandingansuhu < 10pada data ber-NaN menghasilkanfalseuntuk NaN (NaN tidak memenuhi perbandingan apa pun), tetapisah &tetap ditulis agar maksudnya eksplisit: NaN sudah ditangani lapis 0 dan tidak boleh ikut dihitung dua kali. - Baris 35-37 (
m.rataRataSah = ...; z = ...): mean dan std dihitung hanya dari nilai sah. Pembagian./element-wise menyebar z-score ke seluruh 730 posisi; posisi NaN menghasilkan z = NaN yang otomatis gagal melewatiabs(z) > 3. - Baris 38-40 (
m.maskStatistik = sah & (abs(z) > 3);):NaN > 3bernilaifalse, jadisah &di sini murni dokumentasi — namun menulisnya mencegah kejutan ketika aturan diganti misalnyaz > 3tanpaabs. - Baris 43-51 (
fprintf(...)): ringkasan memakaimat2stragar vektor posisi tercetak ringkas satu baris. Nilain=722pada baris terakhir adalah 730 dikurangi 8 NaN — angka silang yang mudah diperiksa.
Menjalankan fungsi pada deret hasil muatDataCuaca memberi hasil:
Output yang Dihasilkan:
=== Deteksi Masalah Data ===
Hari hilang (NaN) : 8 hari, posisi [47 130 262 349 478 555 640 722]
Outlier domain fisik : 3 hari, posisi [88 365 583]
Outlier statistik (|z|>3) : 3 hari, posisi [88 365 583]
Statistik data sah (n=722) : mean 27.3939, std 4.6265Kedua lapis menunjuk tiga posisi yang sama — pada data kita, setiap outlier fisik sekaligus outlier statistik. Ketika kelak dua lapis ini tidak sepakat, lapis fisik-lah yang menentukan perbaikan.
Interpolasi: Mengapa Linear untuk Gap Pendek #
Mengganti titik bermasalah butuh nilai pengganti, dan kandidat terbaik adalah dua tetangga sah terdekat. Fungsi interp1 menarik garis lurus antar titik yang diketahui lalu membaca nilai di posisi yang diminta:
x = [1 2 4 5]; % hari yang diketahui (hari ke-3 hilang)
y = [21.0 22.0 24.0 25.0];
fprintf('interp1 di x=3: %.4f\n', interp1(x, y, 3, 'linear'));Output yang Dihasilkan:
interp1 di x=3: 23.0000Hari ke-3 diisi 23.0 — tepat di tengah 22.0 dan 24.0. Untuk gap satu hari pada data suhu harian, linear sudah memadai karena suhu tidak berubah tajam dalam 24 jam. Pilihan metode lain membawa biaya:
Metode interp1 | Perilaku | Cocok untuk |
|---|---|---|
'linear' | segmen garis antar tetangga | gap pendek, data perlahan berubah |
'nearest' | menyalin tetangga terdekat | data bertingkat/kategori |
'pchip' | kurva mulus terjaga bentuk | gap panjang, hindari osilasi |
'spline' | kurva mulus lewat semua titik | data sangat halus; bisa berlebihan |
bersihkanDeret: Perbaikan Tanpa Mengubah Panjang #
Berikut src/bersihkanDeret.m:
function hasil = bersihkanDeret(hari, suhu, masalah)
% BERSIHKANDERET Mengisi hari hilang dan mengganti outlier dengan interpolasi.
% hasil = bersihkanDeret(hari, suhu, masalah)
%
% masalah adalah struktur keluaran deteksiMasalah. Titik yang diganti:
% - semua NaN (hari hilang),
% - outlier domain fisik (suhu < 10 atau > 40 derajat).
% Outlier statistik murni TIDAK diganti: nilai ekstrem yang masih fisik
% adalah kejadian cuaca nyata, bukan kesalahan data.
%
% Pengganti: interp1 linear dari dua tetangga sah terdekat. Ukuran deret
% tetap 730 hari sehingga analisis musiman dan tren tidak kehilangan baris.
if nargin < 3
error('bersihkanDeret:input', ...
'Sediakan hari, suhu, dan struktur hasil deteksiMasalah.');
end
hari = hari(:);
suhu = suhu(:);
% ---- kumpulkan titik yang perlu diganti ----
titikGanti = masalah.maskNaN | masalah.maskDomain;
valid = ~titikGanti;
if ~any(valid)
error('bersihkanDeret:kosong', ...
'Tidak ada titik valid untuk interpolasi.');
end
% ---- interpolasi linear pada indeks valid ----
suhuBersih = interp1(hari(valid), suhu(valid), hari, 'linear');
if any(isnan(suhuBersih))
error('bersihkanDeret:ujung', ...
'Ada titik tepi (hari pertama/terakhir) yang tidak bisa diinterpolasi.');
end
% ---- statistik sebelum vs sesudah ----
sah = ~masalah.maskNaN; % NaN dikeluarkan, outlier masih dihitung apa adanya
hasil = struct();
hasil.suhuBersih = suhuBersih;
hasil.jumlahDiganti = sum(titikGanti);
hasil.posisiGanti = find(titikGanti);
hasil.statSebelum = struct('mean', mean(suhu(sah)), ...
'min', min(suhu(sah)), 'max', max(suhu(sah)), 'jumlahNilai', sum(sah));
hasil.statSesudah = struct('mean', mean(suhuBersih), ...
'min', min(suhuBersih), 'max', max(suhuBersih), 'jumlahNilai', numel(suhuBersih));
hasil.detailGanti = [hari(titikGanti) suhu(titikGanti) suhuBersih(titikGanti)];
% ---- laporan ----
fprintf('=== Pembersihan Deret ===\n');
fprintf('Titik diganti : %d (%d NaN + %d outlier fisik)\n', ...
hasil.jumlahDiganti, masalah.jumlahNaN, masalah.jumlahDomain);
fprintf(' hari nilai lama nilai baru\n');
for k = 1:hasil.jumlahDiganti
fprintf('%7d %12s %12.4f\n', hasil.detailGanti(k, 1), ...
sprintf('%.4g', hasil.detailGanti(k, 2)), hasil.detailGanti(k, 3));
end
fprintf('Sebelum: mean %.4f, min %.4f, max %.4f (%d nilai sah)\n', ...
hasil.statSebelum.mean, hasil.statSebelum.min, hasil.statSebelum.max, ...
hasil.statSebelum.jumlahNilai);
fprintf('Sesudah: mean %.4f, min %.4f, max %.4f (%d nilai)\n', ...
hasil.statSesudah.mean, hasil.statSesudah.min, hasil.statSesudah.max, ...
hasil.statSesudah.jumlahNilai);
endMari kita bedah baris-baris kode di atas:
- Baris 22-23 (
titikGanti = masalah.maskNaN | masalah.maskDomain;): kebijakan inti fungsi dalam satu ekspresi — hanya NaN dan outlier fisik yang diganti. Outlier statistik murni (nilainya ekstrem tetapi masih fisik) dibiarkan: gelombang panas sungguhan adalah kejadian cuaca, bukan kotoran. - Baris 25-28 (
if ~any(valid)): pengaman ekstrem. Jika seluruh deret bermasalah, tidak ada tetangga untuk belajar — lebih baik menolak dengan pesan jelas daripada mengembalikan deret penuh NaN. - Baris 31 (
suhuBersih = interp1(hari(valid), suhu(valid), hari, 'linear');): satu panggilan menyelesaikan semua. Titik sampel = pasangan (hari, suhu) yang valid; titik query = seluruh hari 1..730. Posisi valid menghasilkan nilai aslinya kembali (query tepat di simpul), posisi diganti menerima nilai interpolasi — fungsi ini sekaligus “memperbaiki” dan “mempertahankan”. - Baris 32-35 (
if any(isnan(suhuBersih))):interp1mengembalikan NaN untuk query di luar jangkauan sampel. Karena hari 1 dan 730 valid di data kita, galat ini tidak muncul — tetapi pemeriksaan tetap dipasang: jika suatu hari hari pertama sendiri hilang, pengguna berhak tahu perbaikannya gagal, bukan menerima NaN diam-diam. - Baris 43-46 (
hasil.statSebelum = .../hasil.statSesudah = ...): statistik dibekukan sebelum dan sesudah — bukti yang dibawa ke laporan.min/maxmengabaikan NaN secara alami, sedangkanmeanpada baris 43 memakaisuhu(sah)yang sudah bersih NaN namun masih memuat outlier — itulah poinnya: kita ingin melihat mean yang terlanjur teracuni. - Baris 47 (
hasil.detailGanti = [hari(titikGanti) ...]): matriks tiga kolom (hari, lama, baru) — laporan per titik sekaligus bahan audit. Tabel inilah yang membuat kesalahan interpolasi terlihat mata. - Baris 53-57 (
for k = 1:hasil.jumlahDiganti ... end): loop cetak 11 baris.sprintf('%.4g', ...)dipakai untuk kolom “nilai lama” agarNaNtercetak sebagaiNaNdan-5tercetak sebagai-5—%.4fakan menghasilkan spasi aneh untuk teks.
Menjalankan pasangan deteksi–pembersihan pada data kita memberi hasil:
Output yang Dihasilkan:
=== Pembersihan Deret ===
Titik diganti : 11 (8 NaN + 3 outlier fisik)
hari nilai lama nilai baru
47 NaN 22.5822
88 -5 26.6168
130 NaN 30.7326
262 NaN 28.6019
349 NaN 21.4473
365 60 21.5811
478 NaN 29.8110
555 NaN 33.8938
583 45 32.6529
640 NaN 27.5908
722 NaN 22.0380
Sebelum: mean 27.3939, min -5.0000, max 60.0000 (722 nilai sah)
Sesudah: mean 27.3643, min 20.6081, max 34.0420 (730 nilai)Membaca Statistik Sebelum dan Sesudah #
Perbandingan dua baris terakhir itu adalah verifikasi pembersihan dalam bentuk paling padat:
| Ukuran | Sebelum | Sesudah | Bacaan |
|---|---|---|---|
| mean | 27.3939 | 27.3643 | tiga nilai liar (total +100 derajat) menarik mean naik ~0.03 |
| min | -5.0000 | 20.6081 | kembali fisik — tak ada suhu di bawah 10 |
| max | 60.0000 | 34.0420 | kembali fisik — tak ada suhu di atas 40 |
| jumlah nilai | 722 sah | 730 | deret kembali utuh tanpa baris dibuang |
Deret bersih kini utuh 730 hari, semuanya fisik, semuanya berhingga. Angka-angkanya siap digambar — Bagian 3 mengubah deret ini menjadi tiga figur yang membuat pola tahunan, moving average, dan perbandingan antar tahun langsung terlihat mata.