Gambar-gambar Bagian 3 memperlihatkan pola; bagian ini mengukurnya. Pertanyaan akhir proyek: berapa derajat per tahun deret ini menghangat, bulan mana yang terpanas dan terdingin, dan apakah perbedaan antar tahunnya nyata? Jawabannya datang dari tiga alat yang sudah kita kuasai — polyfit untuk tren, agregasi logikal untuk indeks bulanan, dan fprintf untuk laporan — dirangkai satu fungsi analisisTren, lalu diorkestrasi jalankanAnalisisCuaca menjadi pipeline penuh yang digembok oleh test. Di ujungnya tertulis laporan_cuaca.txt: satu berkas, seluruh temuan.

Yang kita bangun pada bagian ini:

  • analisisTren — slope tahunan, statistik MA per tahun, indeks bulanan, delta antar tahun,
  • jalankanAnalisisCuaca — orkestrator enam tahap plus penulis laporan otomatis,
  • tests/uji_cuaca.m — sembilan kasus yang mengunci seluruh kontrak proyek.

Tren Linear: dari Slope Harian ke Derajat per Tahun #

polyfit(hari, suhu, 1) mencari garis lurus terbaik yang memotong deret — koefisien pertamanya adalah slope dalam derajat per hari, satuan yang tidak pernah dipakai orang bicara cuaca. Konversinya sederhana namun harus eksplisit: satu tahun = 365.25 hari:

matlab
t = (1:730)';
yy = 27 + 0.001*t;           % tren murni tanpa musiman
p = polyfit(t, yy, 1);
fprintf('slope per hari : %g\n', p(1));
fprintf('slope per tahun: %.4f\n', p(1)*365.25);

Output yang Dihasilkan:

text
slope per hari : 0.001
slope per tahun: 0.3653

Tren 0.001 derajat per hari yang kita tanam di rumus Bagian 1 terbaca kembali sebagai 0.3653 derajat per tahun — slope tak berubah oleh penskalaan, hanya cara membacanya yang berubah.

analisisTren: Empat Ukuran dalam Satu Fungsi #

Berikut src/analisisTren.m (berkas ditutup local function hitungMA30 yang identik dengan Bagian 3, ditulis ulang agar berkas berdiri sendiri):

matlab
function h = analisisTren(hari, suhu)
% ANALISISTREN Menghitung tren tahunan, statistik MA, dan indeks musiman.
%   h = analisisTren(hari, suhu)    % suhu = deret bersih 730 hari
%
% Yang dihitung:
%   1. Tren linear: polyfit derajat 1 pada suhu vs hari;
%      slope dikonversi dari derajat/hari ke derajat/tahun (x 365.25).
%   2. Statistik MA 30 hari per tahun (min, max, mean).
%   3. Indeks musiman bulanan: rata-rata suhu per blok bulan (12 blok
%      pada 365 hari, bulan = floor((doy-1)/30.42) + 1).
%   4. Delta rata-rata tahun 2 terhadap tahun 1.

if nargin < 2
    error('analisisTren:input', 'Sediakan vektor hari dan suhu.');
end
hari = hari(:);
suhu = suhu(:);
n = numel(hari);
if n ~= 730
    error('analisisTren:panjang', ...
        'Deret harus 730 hari (2 tahun), terdeteksi %d.', n);
end

% ---- 1. tren linear ----
p = polyfit(hari, suhu, 1);
h = struct();
h.slopeHari = p(1);
h.slopeTahun = p(1) * 365.25;
h.intersep = p(2);

% ---- 2. statistik MA 30 hari per tahun ----
ma30 = hitungMA30(suhu);
maTahun1 = ma30(30:365);      % MA pertama valid mulai hari ke-30
maTahun2 = ma30(395:730);
h.maTahun1 = struct('min', min(maTahun1), 'max', max(maTahun1), ...
    'mean', mean(maTahun1));
h.maTahun2 = struct('min', min(maTahun2), 'max', max(maTahun2), ...
    'mean', mean(maTahun2));

% ---- 3. indeks musiman bulanan (12 blok pada 365 hari) ----
doy = mod(hari - 1, 365) + 1;
bulan = floor((doy - 1) / 30.42) + 1;
h.rataBulanan = zeros(12, 1);
for b = 1:12
    h.rataBulanan(b) = mean(suhu(bulan == b));
end

% ---- 4. perbandingan rata-rata tahunan ----
h.rataTahun1 = mean(suhu(1:365));
h.rataTahun2 = mean(suhu(366:730));
h.deltaTahun = h.rataTahun2 - h.rataTahun1;

% ---- laporan ----
fprintf('=== Analisis Tren ===\n');
fprintf('Tren linear: %+0.4f derajat/tahun (slope %g derajat/hari)\n', ...
    h.slopeTahun, h.slopeHari);
fprintf('Rata-rata tahun 1: %.4f | MA30: min %.4f, max %.4f, mean %.4f\n', ...
    h.rataTahun1, h.maTahun1.min, h.maTahun1.max, h.maTahun1.mean);
fprintf('Rata-rata tahun 2: %.4f | MA30: min %.4f, max %.4f, mean %.4f\n', ...
    h.rataTahun2, h.maTahun2.min, h.maTahun2.max, h.maTahun2.mean);
fprintf('Delta rata-rata tahun 2 - tahun 1: %+0.4f derajat\n', h.deltaTahun);
fprintf('Indeks musiman bulanan (rata-rata suhu, derajat):\n');
for b = 1:12
    fprintf('   Bulan %2d: %6.2f\n', b, h.rataBulanan(b));
end
end

Mari kita bedah baris-baris kode di atas:

  1. Baris 19-22 (if n ~= 730 ... end): fungsi menolak deret selain 730 hari. Statistik MA per tahun dan indeks bulanan memotong di posisi 365/730 yang tetap — membiarkan panjang berubah berarti diam-diam membandingkan bulan yang tidak sepadan.
  2. Baris 25-29 (p = polyfit(hari, suhu, 1); ... h.slopeTahun = p(1) * 365.25;): satu panggilan polyfit, dua unit: slopeHari untuk diagnostik numerik, slopeTahun untuk dibaca manusia. Intersep disimpan meski jarang dipakai — ia adalah “suhu dasar hari ke-0” yang berguna saat menulis persamaan tren lengkap.
  3. Baris 33-34 (maTahun1 = ma30(30:365); maTahun2 = ma30(395:730);): potongan MA dibuang di dua ujung. MA tahun 1 mulai hari 30 (sebelumnya NaN), dan 29 hari pertama tahun 2 juga NaN milik jendela tahun 1 — potongan 395..730 memastikan setiap nilai MA tahun 2 hanya berisi data tahun 2.
  4. Baris 42 (bulan = floor((doy - 1) / 30.42) + 1;): 365 hari dibagi rata menjadi 12 blok via 30.42 = 365/12. Blok 1 mencakup hari 1-31, blok 12 mencakup hari 336-365 — mendekati kalender tanpa datetime. Angka bulat dari floor lalu dipakai sebagai kunci agregasi.
  5. Baris 44-46 (for b = 1:12 ... mean(suhu(bulan == b))): pola agregasi yang sama dengan profil musiman Bagian 3 — mask logikal sebagai kunci, mean sebagai perekat. Setiap bulan mengumpulkan ±60 nilai (30-31 hari kali 2 tahun).
  6. Baris 49-51 (h.rataTahun1 = ...; h.deltaTahun = ...): pembanding praktis tren. Delta rata-rata dua tahun adalah estimasi tren yang sama sekali tidak tersentuh musiman — pola tahunan yang identik saling meniadakan, menyisakan hanya pergeseran level.

Menjalankan fungsi pada deret bersih memberi hasil:

Output yang Dihasilkan:

text
=== Analisis Tren ===
Tren linear: +0.3612 derajat/tahun (slope 0.000988845 derajat/hari)
Rata-rata tahun 1: 27.1841 | MA30: min 21.3027, max 33.1383, mean 27.6895
Rata-rata tahun 2: 27.5445 | MA30: min 21.6564, max 33.5094, mean 28.0498
Delta rata-rata tahun 2 - tahun 1: +0.3604 derajat
Indeks musiman bulanan (rata-rata suhu, derajat):
   Bulan  1:  21.50
   Bulan  2:  23.04
   Bulan  3:  25.78
   Bulan  4:  28.88
   Bulan  5:  31.54
   Bulan  6:  33.12
   Bulan  7:  33.11
   Bulan  8:  31.54
   Bulan  9:  28.94
   Bulan 10:  25.88
   Bulan  11:  23.24
   Bulan  12:  21.82

Membaca Hasilnya #

Dua estimasi tren yang independen saling menguatkan: slope polyfit 0.3612 derajat/tahun dan delta rata-rata antar tahun 0.3604 derajat. Keduanya mengejar angka yang sama — tren tanam 0.365 derajat/tahun dari rumus Bagian 1 — dengan dua jalan berbeda (regresi seluruh deret vs selisih rata-rata tahunan). Selisih 0.0008 derajat antar keduanya adalah galat sisa derau dan interpolasi: kecil, terukur, jujur. Statistik MA menegaskan arah yang sama: mean MA naik dari 27.69 ke 28.05, min naik dari 21.30 ke 21.66, max dari 33.14 ke 33.51 — tahun 2 hangat merata, bukan hangat musiman.

Indeks bulanan memetakan iklimnya: terpanas bulan 6 dan 7 (33.12 dan 33.11 — musim kemarau Juli), terdingin bulan 1 (21.50 — puncak penghujan Januari), dengan puncak-ke-lembah 11.6 derajat. Amplitudo profil harian Bagian 3 (12.96) sedikit lebih besar karena kurva harian menangkap puncak yang rata-rata bulanan memperhalus — dua ukuran, satu kisaran 11-13 derajat. Pemanasan 0.36 derajat/tahun pada data sintetis ini ringan namun konsisten — pada data nyata BMKG, slope sekecil ini tetap harus dilaporkan dengan rentang ketidakpastian sebelum disebut “tren”.

Orkestrasi dan Laporan Otomatis #

src/jalankanAnalisisCuaca.m merangkai keenam tahap — pembuatan data sampai penulisan laporan_cuaca.txt — dalam satu panggilan:

matlab
function hasil = jalankanAnalisisCuaca(folderOutput)
% JALANKANANALISISCUACA Orkestrasi analisis deret waktu cuaca end-to-end.
%   hasil = jalankanAnalisisCuaca()           -> keluaran di folder kerja
%   hasil = jalankanAnalisisCuaca('folder')   -> keluaran di folder tertentu
%
% Tahapan: buatDataCuaca -> muatDataCuaca -> deteksiMasalah ->
% bersihkanDeret -> visualisasiDeret -> analisisTren -> laporan_cuaca.txt
% Seluruh berkas keluaran (CSV, 3 PNG, laporan txt) ditulis ke folderOutput.

if nargin < 1 || isempty(folderOutput)
    folderOutput = pwd;
end
if exist(folderOutput, 'dir') ~= 7
    mkdir(folderOutput);
end

fprintf('=== Analisis Deret Waktu Cuaca ===\n');

% ---- 1. data sintetis + 2. muat kembali dari CSV ----
fprintf('[1/6] Membuat data sintetis 730 hari...\n');
namaCSV = fullfile(folderOutput, 'data_cuaca.csv');
buatDataCuaca(namaCSV);
fprintf('[2/6] Memuat kembali dari CSV...\n');
d = muatDataCuaca(namaCSV);

% ---- 3. deteksi masalah ----
fprintf('[3/6] Mendeteksi masalah data...\n');
m = deteksiMasalah(d.suhu);

% ---- 4. pembersihan ----
fprintf('[4/6] Membersihkan deret...\n');
b = bersihkanDeret(d.hari, d.suhu, m);

% ---- 5. visualisasi ----
fprintf('[5/6] Membuat visualisasi...\n');
v = visualisasiDeret(d.hari, b.suhuBersih, folderOutput);

% ---- 6. analisis tren ----
fprintf('[6/6] Menganalisis tren dan musiman...\n');
a = analisisTren(d.hari, b.suhuBersih);

% ---- laporan ----
namaLaporan = fullfile(folderOutput, 'laporan_cuaca.txt');
tulisLaporan(namaLaporan, d, m, b, v, a);
fprintf('Laporan ditulis : %s\n', namaLaporan);
fprintf('Analisis selesai.\n');

Bedah intinya: orkestrator menerima satu argumen — folder keluaran — dan menulis semua artefak ke sana (CSV, tiga PNG, laporan), sehingga memanggilnya dari test dengan folder /tmp tidak meninggalkan residu di folder proyek. Setiap tahap mencetak baris [n/6] agar kemajuan terbaca; hasil antara disimpan sebagai struct d, m, b, v, a yang dikembalikan utuh di hasil — pemanggil bisa menyelidiki hasil.tren.slopeTahun tanpa mengulang pipeline. Local function tulisLaporan membuka berkas dengan fopen(..., 'w') lalu memindahkan seluruh fprintf ke tujuan berkas; isinya adalah teks laporan yang sama seperti keluaran konsol. Hasil nyatanya:

text
LAPORAN ANALISIS DERET WAKTU CUACA
==================================
Sumber data : hasil_cuaca/data_cuaca.csv
Periode     : 730 hari (2 tahun sintetis)

1. MASALAH DATA
- Hari hilang (NaN): 8 hari
- Outlier domain fisik: 3 hari
- Outlier statistik (|z|>3): 3 hari

2. PEMBERSIHAN
- Titik diganti interpolasi: 11
- Sebelum: mean 27.3939, min -5.0000, max 60.0000
- Sesudah: mean 27.3643, min 20.6081, max 34.0420

3. TREN TAHUNAN
- Slope linear: +0.3612 derajat/tahun
- Rata-rata tahun 1: 27.1841 derajat
- Rata-rata tahun 2: 27.5445 derajat
- Delta tahun 2 - tahun 1: +0.3604 derajat

4. MUSIMAN
- Amplitudo profil musiman: 12.96 derajat
- Bulan terpanas : bulan 6 (33.12 derajat)
- Bulan terdingin: bulan 1 (21.50 derajat)

Test: Mengunci Seluruh Kontrak #

tests/uji_cuaca.m memeriksa sembilan klaim proyek — dari struktur data sampai determinisme penuh. Pola intinya: setiap klaim menjadi satu ekspresi logika, hasil dikumpulkan, lalu dicetak LULUS/GAGAL. Potongan pertama, klaim deteksi:

matlab
% ======================= deteksi masalah ============================
evalc('m = deteksiMasalah(muat.suhu);');
ok2 = m.jumlahNaN == 8 && isequal(m.posisiNaN, d.posisiNaN) && ...
    muat.jumlahMissing == 8 && m.jumlahDomain == 3 && ...
    isequal(m.posisiDomain, d.posisiOutlier) && m.jumlahStatistik == 3;

Potongan kedua, klaim determinisme:

matlab
% ===================== deterministik 2x ============================
evalc('d2 = buatDataCuaca(fullfile(folderKerja, ''ulang.csv''));');
teks1 = fileread(fullfile(folderKerja, 'data_cuaca.csv'));
teks2 = fileread(fullfile(folderKerja, 'ulang.csv'));
% isequaln memperlakukan NaN sebagai sama (isequal biasa tidak)
ok8 = isequaln(d.suhu, d2.suhu) && isequal(d.suhuAsli, d2.suhuAsli) && ...
    strcmp(teks1, teks2);

Dua detail patut digarisbawahi. Pertama, evalc menjalankan fungsi riuh keluarannya sambil tetap menangkap seluruh fprintf — pipeline diuji penuh, laporan test tetap bersih. Kedua, determinisme diuji dua arah: array di memori dengan isequaln dan isi berkas CSV sebagai teks dengan strcmp — bit-per-bit. Jalankan dari folder tests/:

Output yang Dihasilkan:

text
== uji_cuaca.m ==
LULUS: 730 baris data, kolom hari berurutan 1..730
LULUS: 8 NaN dan 3 outlier fisik/statistik terdeteksi tepat
LULUS: semua NaN terinterpolasi, 3 outlier diganti (masih 10-40)
LULUS: nilai pengganti dekat deret asli (galat < 1.5 derajat)
LULUS: amplitudo profil musiman 11-13 derajat + 3 PNG tersimpan
LULUS: MA 30 hari identik dengan hitungan manual
LULUS: tren ~0.35 derajat/tahun, delta tahun positif, 12 bulan
LULUS: deterministik: dua kali buat identik (NaN-aware)
LULUS: end-to-end: laporan + CSV + gambar di folder keluaran

Amplitudo musiman: 12.9634 | slope/tahun: 0.3612 | delta tahun: 0.3604
Ringkasan: 9 lulus, 0 gagal (total 9)

Baris terakhir sebelum ringkasan mencetak tiga angka kunci — amplitudo, slope, delta — agar manusia bisa memeriksa angka absolut, bukan sekadar lulus/tidak. Semua berkas sementara uji ditulis ke /tmp/uji_cuaca_proyek dan dibersihkan di awal tiap eksekusi: folder konten proyek bebas residu.

Ide Pengembangan #

Proyek ini berhenti di deskripsi; pengembangan alami bergerak ke deteksi dan prediksi:

  • Deteksi anomali berjalan — hitung z-score pada sisa (suhu dikurangi profil musiman), tandai hari yang menyimpang dari musimnya sendiri,
  • Analisis ekstrem — jumlah hari di atas ambang 32 derajat per tahun, durasi gelombang panas terpanjang via diff pada mask ambang,
  • Prediksi sederhana — proyeksikan tren + profil musiman 30 hari ke depan dan ukur galatnya terhadap data yang disimpan,
  • Data nyata — ganti buatDataCuaca dengan CSV stasiun BMKG; seluruh pipeline deteksi–pembersihan–analisis tidak perlu berubah,
  • Ketidakpastian tren — bootstrap blok 30 hari (dengan rng berseed) untuk interval kepercayaan slope,
  • Format laporan — tambahkan tabel markdown atau HTML di tulisLaporan untuk diterbitkan otomatis.

Dengan ini proyek lengkap: data deterministik, pembersihan terverifikasi, visualisasi tiga lapis, tren dan musiman terkuantifikasi, seluruhnya digembok sembilan test yang lulus. Pola yang sama — buat, muat, deteksi, bersihkan, gambar, hitung, laporkan — berlaku untuk deret waktu apa pun: data penjualan, sensor industri, hingga telemetri server.