Deret 730 angka di Command Window tidak bisa dibaca manusia — mata kita tidak dirancang menangkap pola dari kolom angka. Visualisasi mengubah deret bersih hasil Bagian 2 menjadi tiga pertanyaan yang dijawab gambar: bagaimana kelakuan keseluruhan deret, kapan musim panas dan dingin terjadi, dan apakah tahun kedua berbeda dari tahun pertama. Semua figur dibuat headless — dibangun tanpa jendela, disimpan sebagai PNG — sehingga fungsi ini tetap bekerja di server tanpa layar dan di dalam test otomatis.

Yang kita bangun pada bagian ini:

  • visualisasiDeret — satu fungsi, tiga figur, semuanya PNG,
  • moving average 30 hari — penyaring derau yang memisahkan sinyal dari keramaian,
  • profil musiman — agregasi 730 hari menjadi kurva 365 hari-dalam-tahun,
  • perbandingan tahunan — dua subplot dengan sumbu identik.

Tiga Figur, Tiga Pertanyaan #

FigureIsiPertanyaan yang dijawabBerkas
1deret harian (garis tipis) + MA 30 hari (garis tebal)Bagaimana bentuk keseluruhan 2 tahun?deret_harian.png
2rata-rata suhu per hari-dalam-tahunKapan puncak dan lembah musim?profil_musiman.png
3tahun 1 dan tahun 2, subplot bertumpuk, sumbu samaApakah kedua tahun serupa?perbandingan_tahunan.png

Pemisahan ini bukan gaya-gayaan. Figur gabungan 730 titik menjawab pertanyaan bentuk, tetapi terlalu ramai untuk membandingkan dua tahun; profil musiman menjawab pertanyaan waktu, tetapi membutuhkan agregasi yang menghapus tahun. Satu gambar untuk satu pertanyaan — karya pembersih figur yang mahir justru menolak menjejalkan semua ke satu axes.

Moving Average: Menyaring Derau, Menampakkan Sinyal #

Suhu harian bergoyang ±0.5 derajat karena derau dan siklus mingguan. Goyangan itu — gladi bising — menyembunyikan bentuk besar: musiman dan tren. Moving average (MA) mengganti setiap titik dengan rata-rata jendela 30 hari sebelumnya, sehangkan frekuensi tinggi saling meniadakan dan frekuensi rendah lolos. Versi mini dengan jendela 3 hari memperlihatkan mekanismenya:

matlab
s = [20; 22; 24; 26; 28];
n = numel(s);
ma3 = nan(n, 1);
for t = 3:n
    ma3(t) = mean(s(t-2:t));
end
fprintf(' suhu  MA3\n');
for t = 1:n
    if isnan(ma3(t))
        fprintf('%5.0f %5s\n', s(t), 'NaN');
    else
        fprintf('%5.0f %5.1f\n', s(t), ma3(t));
    end
end

Output yang Dihasilkan:

text
 suhu  MA3
   20   NaN
   22   NaN
   24  22.0
   26  24.0
   28  26.0

Dua nilai pertama NaN: jendela 3 hari baru terisi mulai titik ketiga — MA trailing tidak pernah menjangkau ke belakang titik itu. Konsekuensinya, MA 30 hari kita baru valid dari hari ke-30, dan test Bagian 4 memastikan tepat 29 NaN di awal. Jendela 30 hari dipilih karena dua kali lipat siklus mingguan 7 hari (nyaris) dan jauh lebih pendek dari musiman 365 hari — cukup panjang untuk meredam derau, cukup pendek untuk tidak meratakan musim.

Figure 1: Deret Penuh dan MA 30 Hari #

Potongan pertama src/visualisasiDeret.m — persiapan data dan figur pertama:

matlab
if nargin < 3 || isempty(folderSimpan)
    folderSimpan = pwd;
end
hari = hari(:);
suhu = suhu(:);
n = numel(hari);

% ---- moving average 30 hari (trailing) ----
ma30 = hitungMA30(suhu);

% ---- profil musiman: rata-rata per hari-dalam-tahun ----
doy = mod(hari - 1, 365) + 1;          % hari ke-366 kembali ke 1, dst.
profil = zeros(365, 1);
for d = 1:365
    profil(d) = mean(suhu(doy == d));
end

% ---- figure 1: deret penuh + MA 30 hari ----
fig1 = figure('Visible', 'off');
plot(hari, suhu, 'Color', [0.65 0.65 0.65], 'LineWidth', 0.5);
hold on;
plot(hari, ma30, 'b-', 'LineWidth', 2);
hold off;
xlabel('Hari ke-');
ylabel('Suhu (\circC)');
title('Suhu harian 2 tahun dan moving average 30 hari');
legend('Suhu harian', 'MA 30 hari', 'Location', 'northwest');
saveas(fig1, fullfile(folderSimpan, 'deret_harian.png'));
close(fig1);

Mari kita bedah baris-baris kode di atas:

  1. Baris 24 (doy = mod(hari - 1, 365) + 1;): pemetaan hari-dalam-tahun. mod(hari-1, 365) menghasilkan 0..364 berulang; ditambah 1 menjadi 1..365. Hari ke-1, 366, dan 731 semuanya memetakan ke doy 1 — dua tahun kita dilipat menjadi satu tahun untuk agregasi musiman.
  2. Baris 26-28 (for d = 1:365 ... profil(d) = mean(suhu(doy == d));): agregasi via logical indexing. doy == d memilih semua hari (tepatnya dua hari — satu per tahun) yang jatuh pada hari-dalam-tahun yang sama; mean meratakannya. Hasilnya 365 nilai: rata-rata lintas tahun per posisi kalender.
  3. Baris 31 (fig1 = figure('Visible', 'off');): figur dibuat dalam keadaan tak terlihat. Di sesi interaktif MATLAB figur tetap tersimpan di memori; saveas bekerja tanpa jendela pernah terbuka — inilah mode yang aman untuk server dan test.
  4. Baris 32-35 (plot(hari, suhu, 'Color', [0.65 0.65 0.65], ...): deret harian digambar dulu, dengan warna abu-abu dan LineWidth 0.5 — tipis dan pucat, karena perannya latar. hold on menjaga axes tetap terbuka; MA menyusul dengan biru pekat LineWidth 2. Urutan dan ketebalan inilah hierarki visual: yang penting menonjol, yang pendukung menyurut.
  5. Baris 39 (legend('Suhu harian', 'MA 30 hari', 'Location', 'northwest');): legend dipatok di kiri-atas — area kosong di data menurun — supaya tidak menutup kurva. Tanpa Location, MATLAB meletakkannya “best fit” yang kadang pas di atas garis.
  6. Baris 40-41 (saveas(fig1, ...); close(fig1);): simpan lalu tutup. close membebaskan memori figur — kebiasaan wajib untuk fungsi yang membuat banyak figur; tanpa itu, workspace test akan menumpuk lusinan figur tersembunyi.

Figure 2 dan 3: Profil Musiman dan Perbandingan Tahun #

Potongan kedua — dua figur sisanya:

matlab
% ---- figure 2: profil musiman ----
fig2 = figure('Visible', 'off');
plot((1:365)', profil, 'r-', 'LineWidth', 1.5);
xlabel('Hari dalam tahun (1-365)');
ylabel('Suhu (\circC)');
title('Profil musiman: rata-rata per hari-dalam-tahun');
saveas(fig2, fullfile(folderSimpan, 'profil_musiman.png'));
close(fig2);

% ---- figure 3: tahun 1 vs tahun 2 ----
fig3 = figure('Visible', 'off');
subplot(2, 1, 1);
plot(hari(1:365), suhu(1:365), 'Color', [0.65 0.65 0.65], 'LineWidth', 0.5);
hold on;
plot(hari(1:365), ma30(1:365), 'b-', 'LineWidth', 2);
hold off;
xlim([1 365]);
ylim([19 36]);
xlabel('Hari dalam tahun');
ylabel('Suhu (\circC)');
title('Tahun 1: deret harian + MA 30 hari');
legend('Suhu harian', 'MA 30 hari', 'Location', 'northwest');

subplot(2, 1, 2);
plot(hari(366:730) - 365, suhu(366:730), 'Color', [0.65 0.65 0.65], ...
    'LineWidth', 0.5);
hold on;
plot(hari(366:730) - 365, ma30(366:730), 'b-', 'LineWidth', 2);
hold off;
xlim([1 365]);
ylim([19 36]);
xlabel('Hari dalam tahun');
ylabel('Suhu (\circC)');
title('Tahun 2: deret harian + MA 30 hari');
saveas(fig3, fullfile(folderSimpan, 'perbandingan_tahunan.png'));
close(fig3);

Bedahnya:

  1. Baris 45 (plot((1:365)', profil, 'r-', 'LineWidth', 1.5);): profil musiman cukup satu kurva merah sedang — tanpa derau yang perlu diredam, agregasi sudah bekerja. Kurvanya mulus naik-turun: lembah di sekitar hari 1-15 (Desember–Januari) dan puncak di sekitar hari 180-200 (Juli).
  2. Baris 54 dan 66 (subplot(2, 1, 1); / subplot(2, 1, 2);): satu figur dibagi dua axes bertumpuk. Tahun 1 di atas, tahun 2 di bawah — mata membandingkan vertikal lebih teliti daripada overlay warna yang saling menutup.
  3. Baris 67 (plot(hari(366:730) - 365, ...): kunci perbandingan — sumbu-x tahun 2 digeser dengan - 365 sehingga kedua subplot menampilkan rentang x identik 1..365. Tanpa geser ini, tahun 2 tergambar di sumbu 366..730 dan pola musimnya tampak “tergeser setengah layar”.
  4. Baris 59-60 dan 72-73 (xlim([1 365]); ylim([19 36]);): kedua subplot dipatok ke sumbu yang sama persis. Batas y 19..36 dipilih melebihi data (20.6 sampai 34.0) agar kurva tidak menempel tepi. Dengan sumbu bebas, MATLAB memilih batas berbeda per subplot — dan selisih 1 derajat di kedua tahun akan terlihat seperti perbedaan dramatis padahal cuma skala.

Sisa berkas mengemas hasil: struct hasil berisi ma30, doy, profilMusiman, dan daftar nama berkas, lalu fprintf melaporkan ketiga PNG. Fungsi ditutup local function hitungMA30 — trailing 30 hari, 29 NaN di awal — yang juga dijalankan di analisisTren (disalin agar tiap berkas berdiri sendiri). Menjalankan fungsi pada deret bersih:

Output yang Dihasilkan:

text
=== Visualisasi Deret Waktu ===
Gambar 1 disimpan: hasil_cuaca/deret_harian.png
Gambar 2 disimpan: hasil_cuaca/profil_musiman.png
Gambar 3 disimpan: hasil_cuaca/perbandingan_tahunan.png
Amplitudo profil musiman: 12.96 derajat (max 33.81, min 20.85)

Angka terakhir adalah angka penting: amplitudo profil musiman 12.96 derajat — puncak 33.81 di pertengahan tahun, lembah 20.85 di awal tahun. Rentang 11-13 derajat itulah sidik jari iklim tropis Indonesia, dan test Bagian 4 akan menjaganya tetap di rentang tersebut.

Kesalahan Umum Menggambar Deret Waktu #

KesalahanAkibatPerbaikan
plot(hari, suhu, 'o-') — marker penuh730 lingkaran menyatu jadi garis tebal berbintikgaris saja, '-', tanpa marker
Tanpa MAderau menguasai gambar, tren tak terlihattambahkan MA 30 hari sebagai garis tebal
Sumbu y otomatis per subplotperbedaan kecil tampak raksasaxlim/ylim identik antar subplot
Semua seri di satu axes1460 garis saling menutupsubplot terpisah atau agregasi profil
Lupa close(fig)figur menumpuk di memori saat loopclose segera setelah saveas

Tiga figur kini tersimpan dan amplitudo musiman terukur. Yang tersisa dari proyek: mengubah pengamatan visual menjadi angka — slope tren per tahun, indeks bulanan, dan laporan otomatis. Bagian 4 menutup proyek dengan statistik dan test.