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 — interpolasi interp1 linear 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:

AspekDomain fisikStatistik (z-score)
Pertanyaan“Apakah nilai ini mungkin terjadi?”“Apakah nilai ini jauh dari teman-temannya?”
Ambangsuhu < 10 atau suhu > 40 derajat|z| > 3
Kebutuhan datapengetahuan domain (iklim Indonesia)mean dan std data yang sah
Menangkap-5, 60, 45 — salah ketik/sensor rusaknilai ekstrem relatif apa pun
Kelemahannilai 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:

matlab
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:

text
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: 1

Nilai 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:

matlab
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);
end

Mari kita bedah baris-baris kode di atas:

  1. Baris 18 (suhu = suhu(:);): vektor dipaksa menjadi kolom. Semua mask yang dihasilkan akan berbentuk kolom 730x1 — sejajar dengan data.hari dari Bagian 1, sehingga operasi mask1 | mask2 tidak pernah tersandung perbedaan orientasi.
  2. Baris 21-23 (m.maskNaN = isnan(suhu); dst.): isnan menghasilkan mask logikal; find mengubahnya menjadi indeks posisi. Mask untuk mesin (logical indexing), posisi untuk manusia (laporan) — dua representasi data yang sama.
  3. Baris 30 (m.maskDomain = sah & (suhu < batasBawah | suhu > batasAtas);): perbandingan suhu < 10 pada data ber-NaN menghasilkan false untuk NaN (NaN tidak memenuhi perbandingan apa pun), tetapi sah & tetap ditulis agar maksudnya eksplisit: NaN sudah ditangani lapis 0 dan tidak boleh ikut dihitung dua kali.
  4. 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 melewati abs(z) > 3.
  5. Baris 38-40 (m.maskStatistik = sah & (abs(z) > 3);): NaN > 3 bernilai false, jadi sah & di sini murni dokumentasi — namun menulisnya mencegah kejutan ketika aturan diganti misalnya z > 3 tanpa abs.
  6. Baris 43-51 (fprintf(...)): ringkasan memakai mat2str agar vektor posisi tercetak ringkas satu baris. Nilai n=722 pada baris terakhir adalah 730 dikurangi 8 NaN — angka silang yang mudah diperiksa.

Menjalankan fungsi pada deret hasil muatDataCuaca memberi hasil:

Output yang Dihasilkan:

text
=== 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.6265

Kedua 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:

matlab
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:

text
interp1 di x=3: 23.0000

Hari 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 interp1PerilakuCocok untuk
'linear'segmen garis antar tetanggagap pendek, data perlahan berubah
'nearest'menyalin tetangga terdekatdata bertingkat/kategori
'pchip'kurva mulus terjaga bentukgap panjang, hindari osilasi
'spline'kurva mulus lewat semua titikdata sangat halus; bisa berlebihan

bersihkanDeret: Perbaikan Tanpa Mengubah Panjang #

Berikut src/bersihkanDeret.m:

matlab
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);
end

Mari kita bedah baris-baris kode di atas:

  1. 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.
  2. 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.
  3. 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”.
  4. Baris 32-35 (if any(isnan(suhuBersih))): interp1 mengembalikan 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.
  5. Baris 43-46 (hasil.statSebelum = ... / hasil.statSesudah = ...): statistik dibekukan sebelum dan sesudah — bukti yang dibawa ke laporan. min/max mengabaikan NaN secara alami, sedangkan mean pada baris 43 memakai suhu(sah) yang sudah bersih NaN namun masih memuat outlier — itulah poinnya: kita ingin melihat mean yang terlanjur teracuni.
  6. 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.
  7. Baris 53-57 (for k = 1:hasil.jumlahDiganti ... end): loop cetak 11 baris. sprintf('%.4g', ...) dipakai untuk kolom “nilai lama” agar NaN tercetak sebagai NaN dan -5 tercetak sebagai -5 — %.4f akan menghasilkan spasi aneh untuk teks.

Menjalankan pasangan deteksi–pembersihan pada data kita memberi hasil:

Output yang Dihasilkan:

text
=== 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:

UkuranSebelumSesudahBacaan
mean27.393927.3643tiga nilai liar (total +100 derajat) menarik mean naik ~0.03
min-5.000020.6081kembali fisik — tak ada suhu di bawah 10
max60.000034.0420kembali fisik — tak ada suhu di atas 40
jumlah nilai722 sah730deret 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.