Deret waktu cuaca jarang datang bersih: sensor mati satu hari, operator salah ketik 60 alih-alih 26, stasiun mengirim sel kosong. Pada bagian ini kita menyiapkan fondasi proyek: data sintetis 730 hari yang deterministik, ditulis sebagai CSV, lalu dibaca kembali dengan validasi struktur yang tegas. Data sintetis dipilih karena kita bisa menanam masalah pada posisi yang diketahui persis — pembersihan pada Bagian 2 pun bisa dibuktikan benar atau salah lewat test.

Yang kita bangun pada bagian ini:

  • buatDataCuaca — 730 hari suhu dari rumus tetap (tanpa rand), plus 8 hari NaN dan 3 outlier yang diinjeksikan,
  • muatDataCuaca — pembaca CSV dengan guard readmatrix/dlmread dan validasi struktur,
  • format berkas — kontrak kolom hari,suhu yang dijaga oleh error bertipe,
  • peta pipeline — alur enam tahap proyek dari data mentah sampai laporan.

Anatomi Deret Cuaca Harian #

Data cuaca harian paling sederhana punya dua kolom: kapan dan berapa. Kita memakai indeks hari 1..730 alih-alih tanggal kalender agar proyek bekerja di tipe double dan identik di MATLAB maupun GNU Octave — datetime hanya dimiliki MATLAB. Kontrak berkas kita:

KolomIsiSatuanRentang wajar
hariindeks hari sejak awal pengamatanhari (tanpa satuan)1 sampai 730
suhusuhu udara rata-rata harianderajat Celsius10 sampai 40 (iklim Indonesia)

Baris pertama adalah header, sisanya 730 baris data. Nilai yang hilang disimpan sebagai teks NaN sehingga terbaca kembali sebagai NaN — bukan 0 yang diam-diam merusak statistik. Berikut cuplikan berkas hasil buatDataCuaca (baris ke-47 dan ke-88 memperlihatkan masalah yang ditanam):

text
hari,suhu
1,21.26195484
2,21.48997127
3,21.02610881
4,20.8593333
...
47,NaN
...
88,-5

Selisih 12 derajat antara puncak dan lembah musiman adalah kisaran tahunan Indonesia yang sehat; -5 dan 60 mustahil secara fisik — kedua jenis angka ini menjadi alat uji kebenaran pembersihan nanti.

Merancang Data Sintetis yang Deterministik #

Rumus pembangkit dirangkai dari komponen fisik yang bisa dibedah, dan sama sekali tidak memakai angka acak:

KomponenRumusPeran
Baseline27rata-rata tahunan iklim Indonesia
Musiman6*cos(2*pi*(t-183)/365)puncak awal Juli, lembah pergantian tahun; amplitudo 12 derajat
Tren0.001*tpemanasan 0.365 derajat per tahun
Mingguan0.4*sin(2*pi*t/7)variasi siklus 7 hari
Derau0.4*(mod(t*37,100)/100 - 0.5)nilainya unik per hari, tetap deterministik

Komponen terakhir adalah pengganti rand: mod(t*37, 100) memetakan setiap hari ke sisa pembagian yang berbeda — polanya seperti derau namun direproduksi bit-per-bit. Fase musiman sengaja dipusatkan di hari ke-183: puncak jatuh di awal Juli, dan simetri ini membuat komponen musiman tidak mencemari slope tren pada Bagian 4. Masalah lalu ditanam pada posisi tetap:

Hari ke-MasalahNilai tercatat
47, 130, 262, 349NaN (sensor mati)NaN
478, 555, 640, 722NaN (sensor mati)NaN
88outlier dingin tak fisik-5
365outlier panas tak fisik60
583outlier panas tak fisik45

buatDataCuaca: Menulis CSV dengan Guard #

Berikut src/buatDataCuaca.m secara utuh:

matlab
function data = buatDataCuaca(namaFile)
% BUATDATACUACA Membuat deret suhu harian sintetis lalu menyimpannya ke CSV.
%   data = buatDataCuaca()              -> menulis data_cuaca.csv di folder kerja
%   data = buatDataCuaca('cuaca.csv')   -> menulis ke lokasi tertentu
%
% Data 730 hari (2 tahun sintetis) dibentuk dari rumus deterministik,
% TANPA angka acak sama sekali:
%   suhu(t) = 27                     (rata-rata tahunan iklim Indonesia)
%           + 6*cos(2*pi*(t-183)/365)   (musiman: puncak awal Juli, lembah Desember)
%           + 0.001*t                  (tren pemanasan 0.365 derajat/tahun)
%           + 0.4*sin(2*pi*t/7)        (variasi mingguan)
%           + 0.4*(mod(t*37,100)/100 - 0.5)   (derau unik per hari, tetap)
%
% Masalah data sengaja diinjeksikan pada posisi tetap:
%   - 8 hari kosong (NaN)  : hari ke-47, 130, 262, 349, 478, 555, 640, 722
%   - 3 outlier tak fisik  : hari ke-88 (-5), 365 (60), 583 (45)

if nargin < 1 || isempty(namaFile)
    namaFile = 'data_cuaca.csv';
end

nHari = 730;
hari = (1:nHari)';

% ---- komponen rumus (semua deterministik) ----
musiman  = 6*cos(2*pi*(hari - 183)/365);            % periode 365 hari
tren     = 0.001*hari;                              % 0.001 derajat/hari
mingguan = 0.4*sin(2*pi*hari/7);
derau    = 0.4*(mod(hari*37, 100)/100 - 0.5);       % -0.2 .. +0.2, unik per hari
suhuAsli = 27 + musiman + tren + mingguan + derau;

% ---- injeksi masalah pada posisi tetap ----
posisiNaN = [47; 130; 262; 349; 478; 555; 640; 722];
posisiOutlier = [88; 365; 583];
nilaiOutlier = [-5; 60; 45];

suhu = suhuAsli;
suhu(posisiNaN) = NaN;
suhu(posisiOutlier) = nilaiOutlier;

% ---- tulis CSV: header manual, data via writematrix (MATLAB) / dlmwrite (Octave) ----
fid = fopen(namaFile, 'w');
if fid == -1
    error('buatDataCuaca:berkas', 'Tidak dapat menulis berkas: %s', namaFile);
end
fprintf(fid, 'hari,suhu\n');
fclose(fid);

if ~isempty(which('writematrix'))
    writematrix([hari suhu], namaFile, 'WriteMode', 'append');
else
    dlmwrite(namaFile, [hari suhu], 'precision', '%.10g', '-append');
end

data = struct();
data.namaFile = namaFile;
data.hari = hari;
data.suhu = suhu;              % deret dengan masalah (yang ditulis ke CSV)
data.suhuAsli = suhuAsli;      % deret sempurna sebelum injeksi (untuk uji)
data.posisiNaN = posisiNaN;
data.posisiOutlier = posisiOutlier;
data.nilaiOutlier = nilaiOutlier;

fprintf('Data cuaca sintetis: %d hari ditulis ke %s\n', nHari, namaFile);
fprintf('Masalah terinjeksi   : %d hari NaN, %d outlier tak fisik\n', ...
    numel(posisiNaN), numel(posisiOutlier));
end

Mari kita bedah baris-baris kode di atas:

  1. Baris 22-23 (nHari = 730; hari = (1:nHari)';): dua tahun sintetis = 730 hari. Operator ' menjadikan hari vektor kolom — orientasi yang dipertahankan di seluruh proyek.
  2. Baris 26-29 (musiman = ... sampai derau = ...): keempat komponen dihitung sekaligus lewat operasi vektor, tanpa loop — satu baris cos menghasilkan 730 nilai.
  3. Baris 33-35 (posisiNaN = ... dst.): posisi masalah adalah konstanta, bukan hasil pencarian — field data.posisiNaN dan data.posisiOutlier dipakai test untuk memastikan deteksi Bagian 2 menemukan persis titik-titik ini.
  4. Baris 37-39 (suhu(posisiNaN) = NaN;): injeksi lewat index assignment — satu baris menimpa delapan posisi sekaligus; trik yang sama mengganti tiga nilai suhu dengan angka mustahil.
  5. Baris 42-47 (fid = fopen(...) sampai fclose(fid);): header ditulis manual dengan fprintf ke file handle — portabel di semua engine dan membuat baris pertama pasti seragam.
  6. Baris 49-53 (if ~isempty(which('writematrix')) ... end): guard penulis data. which mengembalikan lokasi fungsi jika tersedia dan string kosong jika tidak — MATLAB jatuh ke writematrix, Octave ke dlmwrite, keduanya menulis mode append agar header tidak tertimpa.
  7. Baris 55-62 (data = struct(); ...): keluaran berupa struct. Field suhuAsli (deret sebelum injeksi) adalah “kunci jawaban” untuk test — pada data nyata field ini tidak ada.
  8. Baris 64-66 (fprintf(...)): ringkasan dua baris memberi tahu pemanggil berkas mana yang ditulis dan berapa masalah yang tertanam.

Menjalankan fungsi tersebut:

Output yang Dihasilkan:

text
Data cuaca sintetis: 730 hari ditulis ke data_cuaca.csv
Masalah terinjeksi   : 8 hari NaN, 3 outlier tak fisik

muatDataCuaca: Membaca Kembali dengan Validasi #

Berikut src/muatDataCuaca.m, pembaca dengan guard kebalikan:

matlab
function d = muatDataCuaca(namaFile)
% MUATDATACUACA Membaca CSV deret cuaca harian dan memvalidasi strukturnya.
%   d = muatDataCuaca()            -> membaca data_cuaca.csv di folder kerja
%   d = muatDataCuaca('cuaca.csv') -> membaca lokasi tertentu
%
% Format berkas: baris pertama header "hari,suhu", lalu 730 baris data.
% Nilai suhu yang hilang tersimpan sebagai NaN.
% Struktur keluaran:
%   d.hari, d.suhu, d.jumlahHari, d.jumlahMissing, d.jumlahOutlierDetected

if nargin < 1 || isempty(namaFile)
    namaFile = 'data_cuaca.csv';
end

if exist(namaFile, 'file') ~= 2
    error('muatDataCuaca:berkas', 'Berkas tidak ditemukan: %s', namaFile);
end

% ---- baca matriks: readmatrix (MATLAB R2019a+) / dlmread (Octave) ----
% Baris header dilewati: readmatrix mendeteksinya otomatis, sedangkan
% dlmread perlu indeks baris awal 1 (menghitung dari nol, artinya baris ke-2).
if ~isempty(which('readmatrix'))
    M = readmatrix(namaFile);
else
    M = dlmread(namaFile, ',', 1, 0);
end

% ---- validasi struktural ----
if size(M, 2) ~= 2
    error('muatDataCuaca:kolom', ...
        'CSV harus punya 2 kolom (hari,suhu), terdeteksi %d kolom.', size(M, 2));
end
if size(M, 1) < 2
    error('muatDataCuaca:kosong', 'CSV tidak memuat baris data.');
end

d = struct();
d.namaFile = namaFile;
d.hari = M(:, 1);
d.suhu = M(:, 2);
d.jumlahHari = numel(d.hari);

% kolom hari wajib 1..N berurutan tanpa lompatan
if ~isequal(d.hari, (1:d.jumlahHari)')
    error('muatDataCuaca:urutan', ...
        'Kolom hari harus berisi 1 sampai %d berurutan.', d.jumlahHari);
end

% pemindaian cepat (deteksi rinci ada di deteksiMasalah)
d.jumlahMissing = sum(isnan(d.suhu));
d.jumlahOutlierDetected = sum(~isnan(d.suhu) & (d.suhu < 10 | d.suhu > 40));

fprintf('Berkas dimuat: %s\n', namaFile);
fprintf('Struktur valid : %d hari, kolom hari berurutan 1..%d\n', ...
    d.jumlahHari, d.jumlahHari);
fprintf('Pemindaian awal: %d nilai NaN, %d outlier tak fisik\n', ...
    d.jumlahMissing, d.jumlahOutlierDetected);
end

Mari kita bedah baris-baris kode di atas:

  1. Baris 15-17 (if exist(namaFile, 'file') ~= 2): keberadaan berkas diperiksa sebelum membaca, dan pesan galatnya menyebut nama berkas — pengguna langsung tahu path mana yang salah.
  2. Baris 22-26 (if ~isempty(which('readmatrix')) ...): guard pembaca, kebalikan guard penulis. readmatrix melewati header otomatis; dlmread perlu titik awal — argumen 1, 0 (baris, kolom, berbasis nol) berarti “mulai baris kedua, kolom pertama”.
  3. Baris 29-35 (if size(M, 2) ~= 2 ...): validasi bentuk. CSV yang hanya punya satu kolom ditolak lebih dulu, sebelum menyebabkan galat indexing yang membingungkan di fungsi lain.
  4. Baris 44-47 (if ~isequal(d.hari, (1:d.jumlahHari)')): pemeriksaan rentang sekaligus urutan — perbandingan terhadap (1:N)' menolak hari terlewat, terduplikasi, maupun tidak berurut dalam satu ekspresi.
  5. Baris 50-51 (d.jumlahMissing = sum(isnan(d.suhu)); dst.): pemindaian cepat saat muat — NaN lewat sum(isnan(...)), outlier fisik lewat logical indexing majemuk.
  6. Baris 53-57 (fprintf(...)): tiga baris ringkasan — sumber, struktur, dan isi. Nilai d.jumlahOutlierDetected kita adalah 3, cocok dengan tabel injeksi di atas.

Membaca berkas hasil buatDataCuaca:

Output yang Dihasilkan:

text
Berkas dimuat: data_cuaca.csv
Struktur valid : 730 hari, kolom hari berurutan 1..730
Pemindaian awal: 8 nilai NaN, 3 outlier tak fisik

Validasi terbukti ketika diberi tiga berkas rusak — hilang, kolom tidak pas, hari melompat dari 1 ke 3:

Output yang Dihasilkan:

text
galat 1: Berkas tidak ditemukan: data_tidak_ada.csv
galat 2: CSV harus punya 2 kolom (hari,suhu), terdeteksi 1 kolom.
galat 3: Kolom hari harus berisi 1 sampai 3 berurutan.

Alur Pipeline dari Sini #

Diagram berikut memetakan sisa proyek — setiap kotak adalah satu berkas di src/ yang disusun oleh jalankanAnalisisCuaca:

Bagian 2 membedah cara menemukan titik-titik bermasalah itu — dua lapis pemeriksaan, domain fisik dan statistik — lalu memperbaikinya tanpa mengubah panjang deret.