Dinas Pertanian Kota Sukamaju meminta kita menganalisis rekaman cuaca tahun 2025: satu baris per hari berisi suhu rata-rata, suhu minimum, suhu maksimum, dan curah hujan. Data asli milik kantor tidak boleh kami umumkan, maka kita membangun dataset sintetis dengan pola yang mirip aslinya — gelombang musiman, tren pelan, dan derau harian. Kuncinya adalah generator ber-seed: dengan np.random.default_rng(42), angka yang “acak” itu tetap identik di komputer siapa pun, sebagaimana dibahas pada Chapter default_rng dan Generator dan Reproducibility dan Random Seed.

Bagian ini menyiapkan fondasi seluruh proyek. Kita membangun array 365 × 4, menulisnya ke berkas CSV di folder data/, membacanya kembali dengan np.loadtxt, lalu memeriksa struktur awalnya sebelum dipakai menghitung apa pun. Pada proyek sungguhan, langkah-langkah ini kita simpan sebagai src/persiapan.py agar keempat bagian proyek memakai definisi data yang sama.

Membangun Dataset Cuaca Sintetis #

Rumus datanya sederhana namun menyerupai cuaca tropis. Suhu rata-rata mengikuti gelombang kosinus setahun penuh dengan amplitudo 2.5 °C, ditambah tren pemanasan kecil 0.001 °C per hari dan derau harian. Suhu minimum dan maksimum mengikuti suhu rata-rata dengan jarak tetap, sedangkan curah hujan berupa nilai acak yang dipotong pada nol lalu diperbesar saat musim hujan di awal dan akhir tahun:

python
import numpy as np

# Pembangun data cuaca Kota Sukamaju, tahun 2025 — dipakai di 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])

print(data.shape, data.dtype)
print(np.round(data[:3], 2))

Output yang Dihasilkan:

text
(365, 4) float64
[[24.74 20.62 29.34 23.09]
 [25.25 20.58 28.21 27.29]
 [24.61 20.92 28.59 14.38]]

Mari kita bedah pembangun data ini:

  1. Baris 4 (rng = np.random.default_rng(42)): generator acak PCG64 dibuat dengan seed 42; seluruh derau proyek keluar dari objek ini sehingga hasilnya dapat direproduksi.
  2. Baris 6 (noise = ...): satu panggilan normal menghasilkan matriks 365 × 3 berisi derau standar, lalu dikalikan vektor simpangan baku per kolom (0.8, 0.6, 0.8) sehingga tiap besaran punya keragaman yang berbeda.
  3. Baris 7 (musim = ...): -np.cos(2πt/365) bernilai -1 pada 1 Januari dan +1 di pertengahan tahun; bentuk ini dipilih agar gelombang musiman tidak tertukar dengan tren linear saat regresi di Bagian 4.
  4. Baris 8-10: suhu rata-rata disusun dari dasar 27.0 °C, gelombang musiman, tren, dan derau; suhu minimum serta maksimum digeser -3.5 °C dan +4.0 °C dari suhu rata-rata ditambah derau sendiri.
  5. Baris 11-12: derau hujan yang bernilai negatif dipangkas ke 0.0 oleh np.clip (lihat Chapter Clamp dan clip), lalu dikalikan faktor 1 + 0.7·cos yang membuat awal dan akhir tahun lebih basah.
  6. Baris 13 (np.column_stack): empat array 1-D sepanjang 365 disusun berdampingan menjadi matriks 365 × 4, satu kolom per besaran.

Bentuk paling portabel untuk pertukaran data tabular adalah CSV. Kita menulis matriks dengan np.savetxt lengkap dengan baris header, membacanya kembali dengan np.loadtxt (dibahas tuntas pada Chapter Membaca dan Menulis CSV), lalu membandingkan hasilnya dengan data di memori:

python
import numpy as np

# Pembangun data (identik dengan blok sebelumnya).
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])

np.savetxt("cuaca_sukamaju.csv", data, delimiter=",", fmt="%.2f",
           header="suhu_rata,suhu_min,suhu_max,curah_hujan", comments="")
data_kembali = np.loadtxt("cuaca_sukamaju.csv", delimiter=",", skiprows=1)
print("identik persis:", np.array_equal(data, data_kembali))
print("allclose atol=0.01:", np.allclose(data, data_kembali, atol=0.01))
with open("cuaca_sukamaju.csv") as f:
    for i, baris in enumerate(f):
        if i >= 2:
            break
        print(baris.rstrip())

Output yang Dihasilkan:

text
identik persis: False
allclose atol=0.01: True
suhu_rata,suhu_min,suhu_max,curah_hujan
24.74,20.62,29.34,23.09

Yang perlu kita cermati:

  1. savetxt dengan header=... dan comments="": baris pertama berkas berisi nama kolom tanpa tanda #; fmt="%.2f" membulatkan setiap nilai menjadi dua desimal agar berkas mudah dibaca manusia.
  2. loadtxt dengan skiprows=1: baris header dilewati sehingga yang terbaca adalah data numerik murni, kembali sebagai array float64 berbentuk (365, 4).
  3. array_equal menghasilkan False: format %.2f membulatkan nilai, misalnya 24.7439 menjadi 24.74; selisihnya di bawah satu persen satuan, itulah sebabnya np.allclose dengan atol=0.01 mengembalikan True.

Pemeriksaan Struktur Awal #

Sebelum data dipakai, kita lakukan pemeriksaan kesehatan singkat: bentuk dan dtype sesuai ekspektasi, tidak ada nan yang lolos, dan semua nilai berada dalam rentang yang wajar untuk data cuaca:

python
import numpy as np

# Pembangun data (identik dengan blok sebelumnya).
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])

print("shape :", data.shape)
print("dtype :", data.dtype)
print("sel nan:", np.isnan(data).sum())
bawah = np.array([10.0, 10.0, 15.0, 0.0])
atas = np.array([45.0, 45.0, 50.0, 250.0])
print("dalam rentang wajar:", bool(np.all((data >= bawah) & (data <= atas))))
print("min per kolom:", np.round(data.min(axis=0), 2))
print("max per kolom:", np.round(data.max(axis=0), 2))

Output yang Dihasilkan:

text
shape : (365, 4)
dtype : float64
sel nan: 0
dalam rentang wajar: True
min per kolom: [23.29 19.32 26.71  0.  ]
max per kolom: [31.24 28.75 35.84 35.83]

Tiga hal dari pemeriksaan ini:

  1. data.shape dan data.dtype: 365 baris untuk 365 hari tahun 2025 dan 4 kolom untuk empat besaran; dtype float64 adalah pilihan wajar untuk pengukuran fisik (lihat Chapter Atribut shape dan Atribut dtype).
  2. np.isnan(data).sum() bernilai 0: data sintetis kita memang bersih; pada Bagian 2 kita akan menyisipkan kegagalan sensor secara sengaja agar latihan pembersihan terasa nyata.
  3. Perbandingan data >= bawah dan data <= atas: broadcasting membandingkan setiap sel dengan batas kolomnya, operator & menggabungkan kedua mask, dan np.all memastikan tidak ada satu pun pelanggaran.

Ringkasan kolom dataset yang akan kita pakai sepanjang proyek:

KolomArtiSatuanRentang teramati
suhu_ratasuhu udara harian rata-rata°C23.29 - 31.24
suhu_minsuhu minimum harian°C19.32 - 28.75
suhu_maxsuhu maksimum harian°C26.71 - 35.84
curah_hujancurah hujan harianmm0.00 - 35.83

Data sudah siap di folder data/ dan strukturnya sehat. Bagian berikutnya, Bagian 2: Membersihkan Data, sengaja merusak sebagian pembacaan sensor agar kita berlatih mendeteksi dan memperbaikinya.