Bagian terakhir proyek ini menjawab pertanyaan yang biasanya diajukan pengambil kebijakan: apakah suhu Kota Sukamaju bergerak naik, hari-hari mana yang menyimpang jauh dari normal, dan bagaimana keseluruhan temuan tahun 2025 dirangkum untuk laporan. Kita tetap bekerja pada data bersih yang sama — satu pembangun dataset, satu seed, sehingga setiap angka di laporan ini dapat diulang pembaca.

Tiga alat baru dipakai di sini, dan semuanya telah dibahas di section sebelumnya: regresi linear lewat np.linalg.lstsq dari Chapter Least Squares, penyaringan anomali dengan boolean indexing dari Chapter Boolean Indexing, serta pemeriksaan otomatis dari Chapter numpy.testing. Hasil akhirnya adalah laporan ringkas bertabel yang menggabungkan temuan Bagian 1 sampai 4.

Mengukur Tren Suhu dengan Least Squares #

Gelombang musiman membuat tren sulit dibaca mata: suhu naik hingga pertengahan tahun lalu turun, padahal tren sejati bertambah pelan setiap hari. Regresi linear memisahkan keduanya. Kita menyusun matriks perancang A berisi kolom t dan kolom konstanta, lalu membiarkan lstsq mencari kemiringan dan intersep yang paling mendekati data:

python
import numpy as np

# Pembangun data cuaca 2025 (data bersih, tanpa sentinel).
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])

A = np.vstack([t, np.ones(365)]).T
koef = np.linalg.lstsq(A, suhu_rata, rcond=None)[0]
slope, intersep = koef
print(f"slope    : {slope:.6f} °C/hari")
print(f"intersep : {intersep:.2f} °C")
print(f"tren     : {slope * 3650:+.2f} °C per dekade")
print("selisih rata-rata 30 hari akhir vs awal:",
      f"{suhu_rata[-30:].mean() - suhu_rata[:30].mean():+.2f} °C")

Output yang Dihasilkan:

text
slope    : 0.001362 °C/hari
intersep : 26.94 °C
tren     : +4.97 °C per dekade
selisih rata-rata 30 hari akhir vs awal: +0.47 °C

Mari kita bedah:

  1. np.vstack([t, np.ones(365)]).T: dua kolom disusun lalu ditransposkan menjadi matriks 365 × 2; kolom pertama adalah hari, kolom kedua konstanta satu untuk intersep.
  2. np.linalg.lstsq(A, suhu_rata, rcond=None)[0]: menyelesaikan persamaan A · koef ≈ suhu_rata dalam arti kuadrat terkecil; elemen [0] adalah vektor koefisien (slope, intersep).
  3. slope * 3650: mengonversi kemiringan per hari menjadi per dekade — bahasa yang lebih bermakna bagi pembaca laporan.
  4. slope ≈ 0.001362: dataset memang ditanami tren 0.001 °C per hari (≈ +3.7 °C per dekade); estimasi sedikit meleset karena 365 sampel dengan derau 0.8 °C, dan selisih rata-rata 30 hari akhir versus awal (+0.47 °C) memberi dukungan kasar pada arah yang sama.

Angka +4.97 °C per dekade jauh di atas tren iklim nyata — wajar, karena tren pada data simulasi sengaja diperbesar agar polanya terlihat dalam satu tahun saja. Yang diuji di sini adalah metodenya: dengan data panjang dan derau yang sama, formula yang identik itulah yang dipakai pada data sungguhan.

Menyaring Anomali Lebih dari 2 Std #

Anomali adalah selisih suhu harian terhadap rata-rata tahunan. Hari yang anomali lebih dari dua simpangan baku jarang terjadi secara kebetulan — untuk derau normal, sekitar 4.6% hari — sehingga layak ditandai. Kita memakai Chapter Variance dan Standard Deviation untuk ambangnya dan boolean indexing untuk menyaringnya:

python
import numpy as np

# Pembangun data cuaca 2025 (data bersih, tanpa sentinel).
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])

anomali = suhu_rata - suhu_rata.mean()
ambang = 2 * suhu_rata.std()
tanggal = np.arange(np.datetime64("2025-01-01"), np.datetime64("2026-01-01"))
ekstrem = np.abs(anomali) > ambang
print("ambang :", f"{ambang:.2f} °C")
print("jumlah hari anomali > 2 std:", np.count_nonzero(ekstrem))
for i in np.where(ekstrem)[0]:
    arah = "di atas" if anomali[i] > 0 else "di bawah"
    print(f"{tanggal[i]}  {arah} rata-rata {abs(anomali[i]):.2f} °C")

Output yang Dihasilkan:

text
ambang : 3.82 °C
jumlah hari anomali > 2 std: 3
2025-01-21  di bawah rata-rata 3.87 °C
2025-07-15  di atas rata-rata 4.05 °C
2025-12-09  di bawah rata-rata 3.90 °C

Catatan pembacaan:

  1. anomali = suhu_rata - suhu_rata.mean(): pengurangan array dengan skalar menggeser seluruh deret sehingga rata-ratanya nol.
  2. np.abs(anomali) > ambang: mask boolean yang menyala untuk hari yang terlalu panas atau terlalu dingin; np.count_nonzero menjumlahkan hari-hari tersebut.
  3. np.where(ekstrem)[0]: mengubah mask menjadi indeks agar tanggalnya bisa dicetak satu per satu.
  4. Hasilnya hanya 3 hari, dan dua di antaranya bertepatan dengan rekaman Bagian 3 — hari terpanas 15 Juli dan hari terdingin 9 Desember. Anomali menangkap persis hari-hari yang layak diperiksa ulang oleh pemantau cuaca.

Verifikasi Proyek #

Proyek data belum selesai sebelum perhitungannya teruji. Blok berikut meniru isi tests/uji_statistik.py: membangun ulang data dari seed yang sama, lalu memastikan invarian-invarian kunci dengan assert dan numpy.testing sebagaimana dipraktikkan pada Chapter assert_allclose dan Reproducible Numerical Tests:

python
import numpy as np

# Pembangun data cuaca 2025 — sumber tunggal seluruh proyek.
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])

assert data.shape == (365, 4)
assert data.dtype == np.float64
assert np.all(suhu_max > suhu_min)
assert np.all(curah_hujan >= 0.0)
assert np.isnan(data).sum() == 0
rata_bulanan = suhu_rata[:360].reshape(12, 30).mean(axis=1)
np.testing.assert_allclose(rata_bulanan.mean(), suhu_rata[:360].mean())
prediksi = 27.0 + 2.5 * musim + 0.001 * t
print("residual maksimum:", round(np.abs(suhu_rata - prediksi).max(), 2))
print("Semua pemeriksaan lulus")

Output yang Dihasilkan:

text
residual maksimum: 2.54
Semua pemeriksaan lulus

Setiap baris pemeriksaan menjaga satu janji:

  1. Shape dan dtype: dataset tetap 365 × 4 float64 — fondasi bagi semua perhitungan lain.
  2. suhu_max > suhu_min untuk semua hari: hukum fisika sederhana yang seharusnya tak pernah dilanggar data.
  3. curah_hujan >= 0 dan tanpa nan: hasil pemangkasan dan pembersihan tetap berlaku.
  4. assert_allclose rata-rata bulanan: rata-rata dari 12 rata-rata blok sama dengan rata-rata 360 hari langsung — bukti bahwa pembagian blok tidak menggeser angka.
  5. residual maksimum 2.54: selisih terbesar data terhadap rumus pembangunnya sepenuhnya berasal dari derau; tidak ada langkah analisis yang mengubah data.

Laporan #

Seluruh temuan proyek merangkum dalam dua tabel berikut. Angka-angka bulanan memakai pembagian 12 × 30 hari sesuai kesepakatan Bagian 3:

BulanSuhu rata-rata (°C)Curah hujan (mm)
Januari24.63360.8
Februari25.22280.2
Maret26.31157.2
April27.83153.2
Mei28.7495.6
Juni29.5063.3
Juli29.5951.2
Agustus28.8876.8
September28.07155.2
Oktober26.77208.0
November25.86346.1
Desember25.26295.4

Tabel bulanan hanya mencakup 360 hari pertama; total hujan tahunan pada tabel ringkasan di bawah dihitung dari 365 hari penuh.

IndikatorNilai
Rata-rata suhu tahunan27.19 °C
Bulan terpanasJuli (29.59 °C)
Bulan terdinginJanuari (24.63 °C)
Hari terpanas15 Juli (31.24 °C)
Hari terdingin9 Desember (23.29 °C)
Total curah hujan setahun2338.0 mm
Hari hujan (> 1 mm)257 (70.4%)
Hujan harian terbesar28 Desember (35.8 mm)
Tren suhu (estimasi lstsq)+0.001362 °C/hari ≈ +4.97 °C/dekade
Hari dengan anomali > 2 std3

Tiga kesimpulan layak ditulis di halaman pertama laporan. Pertama, suhu Kota Sukamaju mengikuti gelombang musiman dengan rentang hampir 5 °C antara Januari dan Juli, plus tren pemanasan yang terukur meski datanya satu tahun. Kedua, hujan bergerak berlawanan dengan suhu: Januari terbasah (360.8 mm) justru saat suhu terendah, Juli terkering (51.2 mm) saat suhu tertinggi — pola kemarau pertengahan tahun yang khas. Ketiga, hanya tiga hari yang menyimpang lebih dari 2 std dan semuanya sudah teridentifikasi dengan tanggalnya, sehingga pemantauan berikutnya bisa berfokus pada hari serupa.

Sebagai proyek utuh, struktur foldernya kini lengkap: data/cuaca_sukamaju.csv sebagai bahan baku, src/persiapan.py dan src/pola.py sebagai kode analisis, tests/uji_statistik.py dari blok verifikasi di atas, serta laporan/laporan-cuaca.md berisi dua tabel di bagian ini.