Laporan cuaca yang kita baca sehari-hari berawal dari berkas pengamatan sederhana: satu baris per hari per stasiun berisi suhu, kelembapan, dan curah hujan. Proyek ini memakai dataset pengukuran cuaca harian Bandung, Medan, dan Yogyakarta sepanjang Januari-Maret 2026 yang dibuat oleh skrip deterministik, sehingga setiap angkanya dapat Anda reproduksi sendiri. Seperti berkas ekspor pada umumnya, datanya tidak rapi: baris tanggal hilang, suhu 55 derajat, curah hujan negatif, kelembapan kosong, dan baris ganda sengaja ditanam sebagai bahan latihan. Pada bagian pembuka ini kita memuat cuaca.csv dengan read_csv(), memetakan enam masalahnya lewat pemeriksaan awal, lalu menjadikan kolom tanggal sebagai datetime index — fondasi Bagian 2 sampai Bagian 4.
Dataset Cuaca yang Dapat Direproduksi #
Skrip buat_cuaca.py membangun dataset memakai np.random.default_rng(13) sehingga hasilnya identik di komputer mana pun. Dataset dasarnya 270 baris — tiga kota dikali 90 hari, 1 Januari sampai 31 Maret 2026. Tujuh baris dihapus untuk mensimulasikan stasiun yang tidak merekam (Bandung 17-18 Januari, Yogyakarta 8 Februari, Medan 14-17 Februari), lalu lima salinan baris ditambahkan sebagai duplikat. Hasil akhirnya 268 baris mentah dengan urutan acak agar masalah tersebar.
Enam kolomnya: tanggal (teks, 1 Januari-31 Maret 2026), kota (Bandung, Medan, atau Yogyakarta), suhu_min dan suhu_max (derajat Celsius), kelembapan (persen), serta curah_hujan (mm).
Enam masalah kualitas data ditanam terkendali; rekap lengkapnya ada pada tabel di akhir bagian ini. Curah hujan dibangkitkan dari distribusi gamma — banyak hari kering, sesekali hujan deras — meniru pola musim hujan tropis. Masalah nilai sengaja tidak ditanam pada 1 Januari dan 31 Maret; alasannya baru masuk akal setelah interpolasi dibahas di Bagian 2.
"""Generator dataset cuaca harian tiga kota Indonesia, Januari-Maret 2026.
Dataset mentah dibuat secara deterministik (seed 13) dan sengaja memuat
masalah kualitas data untuk dilatih:
1. baris (tanggal, kota) hilang -- stasiun cuaca tidak merekam,
2. suhu_max ekstrem 55.0 derajat yang tidak masuk akal,
3. suhu_min lebih besar dari suhu_max (sensor tertukar),
4. curah_hujan negatif,
5. kelembapan kosong (NaN),
6. baris duplikat.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
SEED = 13
KOTA = ["Bandung", "Medan", "Yogyakarta"]
KOLOM = ["tanggal", "kota", "suhu_min", "suhu_max", "kelembapan", "curah_hujan"]
PARAM_KOTA = {
"Bandung": {"suhu_min": 17.5, "suhu_max": 27.0, "kelembapan": 78, "peluang_hujan": 0.60},
"Medan": {"suhu_min": 23.0, "suhu_max": 32.5, "kelembapan": 82, "peluang_hujan": 0.45},
"Yogyakarta": {"suhu_min": 22.5, "suhu_max": 31.5, "kelembapan": 75, "peluang_hujan": 0.50},
}
# stasiun cuaca "offline": baris (kota, tanggal) ini dihapus dari dataset
TANGGAL_HILANG = {
"Bandung": ["2026-01-17", "2026-01-18"],
"Yogyakarta": ["2026-02-08"],
"Medan": ["2026-02-14", "2026-02-15", "2026-02-16", "2026-02-17"],
}
def buat_cuaca() -> pd.DataFrame:
"""Menghasilkan DataFrame mentah dengan masalah yang dirancang."""
rng = np.random.default_rng(SEED)
hari = pd.date_range("2026-01-01", "2026-03-31", freq="D")
n = len(hari)
bagian = []
for kota in KOTA:
p = PARAM_KOTA[kota]
suhu_min = np.round(p["suhu_min"] + rng.normal(0, 0.7, n), 1)
suhu_max = np.round(p["suhu_max"] + rng.normal(0, 1.2, n), 1)
kelembapan = np.clip(np.round(p["kelembapan"] + rng.normal(0, 4, n)), 40, 99)
hujan = np.where(
rng.random(n) < p["peluang_hujan"],
np.round(rng.gamma(1.2, 8.0, n), 1),
0.0,
)
bagian.append(pd.DataFrame({
"tanggal": hari.strftime("%Y-%m-%d"),
"kota": kota,
"suhu_min": suhu_min,
"suhu_max": suhu_max,
"kelembapan": kelembapan.astype("int64"),
"curah_hujan": hujan,
}))
df = pd.concat(bagian, ignore_index=True)
# 1) hapus baris (kota, tanggal) yang stasiunnya tidak merekam
mask_hilang = pd.Series(False, index=df.index)
for kota, daftar in TANGGAL_HILANG.items():
mask_hilang |= (df["kota"] == kota) & df["tanggal"].isin(daftar)
df = df[~mask_hilang].reset_index(drop=True)
# kandidat baris utk masalah nilai: bukan 1 Januari / 31 Maret
# (agar NaN hasil perbaikan selalu punya tetangga sebelum-sesudah)
kandidat = df.index[~df["tanggal"].isin(["2026-01-01", "2026-03-31"])].to_numpy()
# 2) suhu_max ekstrem tidak masuk akal (3 baris)
iks = rng.choice(kandidat, size=3, replace=False)
df.loc[iks, "suhu_max"] = 55.0
kandidat = np.setdiff1d(kandidat, iks)
# 3) suhu_min > suhu_max, sensor tertukar (2 baris)
iks = rng.choice(kandidat, size=2, replace=False)
df.loc[iks, ["suhu_min", "suhu_max"]] = df.loc[
iks, ["suhu_max", "suhu_min"]
].to_numpy()
kandidat = np.setdiff1d(kandidat, iks)
# 4) curah_hujan negatif (4 baris)
iks = rng.choice(kandidat, size=4, replace=False)
df.loc[iks, "curah_hujan"] = -np.round(rng.uniform(0.1, 2.5, 4), 1)
kandidat = np.setdiff1d(kandidat, iks)
# 5) kelembapan kosong / NaN (10 baris)
iks = rng.choice(kandidat, size=10, replace=False)
df.loc[iks, "kelembapan"] = np.nan
# 6) baris duplikat: 5 salinan persis dari baris berkelembapan lengkap,
# agar jumlah NaN tidak ikut bertambah
iks = rng.choice(df.index[df["kelembapan"].notna()], size=5, replace=False)
df = pd.concat([df, df.loc[iks]], ignore_index=True)
# acak urutan baris agar masalah tersebar, lalu rapikan index
urutan = rng.permutation(len(df))
return df.iloc[urutan].reset_index(drop=True)
if __name__ == "__main__":
import tempfile
from pathlib import Path
df = buat_cuaca()
with tempfile.TemporaryDirectory() as folder:
berkas = Path(folder) / "cuaca.csv"
df.to_csv(berkas, index=False)
print(
f"cuaca.csv siap: {df.shape[0]} baris x {df.shape[1]} kolom "
f"({berkas.stat().st_size} byte)"
)Output yang Dihasilkan:
cuaca.csv siap: 268 baris x 6 kolom (10353 byte)Skrip ini tidak meninggalkan berkas di disk: cuaca.csv ditulis di dalam tempfile.TemporaryDirectory() semata untuk melaporkan ukurannya, lalu folder sementara itu terhapus otomatis. Angka yang dicetak — 268 baris x 6 kolom — adalah bentuk akhir dataset mentah.
Memuat CSV dan Memeriksa Struktur #
Dataset diperlakukan seperti berkas stasiun sungguhan: tulis ke CSV, muat ulang dengan pd.read_csv(), lalu periksa strukturnya. Skrip berikut mencetak bentuk data, tipe tiap kolom, lima baris pertama, statistik ringkas, rekap masalah, dan konversi tanggal menjadi datetime index.
"""Memuat cuaca.csv lalu memeriksa struktur dan masalah awal dataset."""
from pathlib import Path
import tempfile
import pandas as pd
from buat_cuaca import buat_cuaca
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as folder:
berkas = Path(folder) / "cuaca.csv"
buat_cuaca().to_csv(berkas, index=False)
df = pd.read_csv(berkas)
print("== Struktur dataset ==")
print(f"bentuk: {df.shape[0]} baris x {df.shape[1]} kolom")
print(df.dtypes.to_string())
print("\n== Lima baris pertama ==")
print(df.head(5).to_string(index=False))
print("\n== Statistik ringkas kolom angka (dibulatkan 2 desimal) ==")
print(
df[["suhu_min", "suhu_max", "kelembapan", "curah_hujan"]]
.describe()
.round(2)
.to_string()
)
print("\n== Masalah yang ditemukan ==")
print(f"baris duplikat : {df.duplicated().sum()}")
print(f"suhu_max di atas 45 derajat: {(df['suhu_max'] > 45).sum()}")
print(f"suhu_min > suhu_max : {(df['suhu_min'] > df['suhu_max']).sum()}")
print(f"curah_hujan negatif : {(df['curah_hujan'] < 0).sum()}")
print(f"kelembapan kosong (NaN) : {df['kelembapan'].isna().sum()}")
print("tanggal unik per kota:")
print(df.groupby("kota")["tanggal"].nunique().to_string())
print("\n== Kolom tanggal menjadi datetime index ==")
df["tanggal"] = pd.to_datetime(df["tanggal"])
print(f"dtype tanggal: {df['tanggal'].dtype}")
df = df.sort_values("tanggal").set_index("tanggal")
print(f"index: {df.index.dtype}, {df.index.min().date()} s.d. {df.index.max().date()}")
print(f"baris Februari (df.loc[\"2026-02\"]): {len(df.loc['2026-02'])}")Output yang Dihasilkan:
== Struktur dataset ==
bentuk: 268 baris x 6 kolom
tanggal str
kota str
suhu_min float64
suhu_max float64
kelembapan float64
curah_hujan float64
== Lima baris pertama ==
tanggal kota suhu_min suhu_max kelembapan curah_hujan
2026-02-05 Medan 22.8 30.1 83.0 0.0
2026-03-24 Medan 23.1 32.2 82.0 2.6
2026-01-26 Medan 23.1 32.5 91.0 0.0
2026-02-28 Bandung 17.2 26.3 72.0 25.7
2026-02-02 Medan 23.8 33.9 78.0 9.3
== Statistik ringkas kolom angka (dibulatkan 2 desimal) ==
suhu_min suhu_max kelembapan curah_hujan
count 268.00 268.0 258.00 268.00
mean 21.10 30.6 78.57 5.42
std 2.73 3.7 5.03 8.68
min 15.30 21.5 67.00 -2.10
25% 18.00 28.0 75.00 0.00
50% 22.30 31.1 79.00 1.00
75% 23.00 32.4 82.00 7.82
max 32.00 55.0 91.00 51.40
== Masalah yang ditemukan ==
baris duplikat : 5
suhu_max di atas 45 derajat: 3
suhu_min > suhu_max : 2
curah_hujan negatif : 4
kelembapan kosong (NaN) : 10
tanggal unik per kota:
kota
Bandung 88
Medan 86
Yogyakarta 89
== Kolom tanggal menjadi datetime index ==
dtype tanggal: datetime64[us]
index: datetime64[us], 2026-01-01 s.d. 2026-03-31
baris Februari (df.loc["2026-02"]): 81Mari kita telaah bagian-bagian pentingnya:
- Blok
with tempfile.TemporaryDirectory(): dataset ditulis kecuaca.csvlalu dimuat ulang denganpd.read_csv(); di pekerjaan nyata, ganti argumennya dengan lokasi berkas Anda. df.dtypes: dua kolom teks bertipestr— bawaan pandas 3.0, bukan lagiobjectseperti pandas 2.x. Empat kolom angka semuanyafloat64;kelembapanyang seharusnya bilangan bulat ikut menjadifloat64karena sel kosong dibaca sebagai NaN, dan NaN hanya dapat tinggal di kolom float.describe(): statistik ini memperlihatkan bukti masalah —suhu_maxbermaksimum 55.0 derajat dancurah_hujanberminimum -2.10, keduanya mustahil secara fisik. Kolomkelembapanhanya terhitung 258 dari 268 baris, artinya sepuluh nilai hilang.groupby("kota")["tanggal"].nunique(): tanggal unik per kota hanya 88, 86, dan 89 dari target 90 hari; selisihnya tujuh baris hilang.nunique()dipilih karena menghitung tanggal berbeda, tak terkecoh baris duplikat.
| Masalah | Jumlah | Cara deteksi |
|---|---|---|
| Baris duplikat | 5 | df.duplicated() |
suhu_max di atas 45 derajat | 3 | (df["suhu_max"] > 45).sum() |
suhu_min > suhu_max | 2 | perbandingan dua kolom |
curah_hujan negatif | 4 | (df["curah_hujan"] < 0).sum() |
kelembapan kosong (NaN) | 10 | df["kelembapan"].isna() |
| Tanggal hilang per kota | 7 | selisih nunique() dengan 90 |
Kolom tanggal Menjadi datetime Index #
Analisis cuaca berpangkal pada waktu, sehingga kolom tanggal perlu berubah dari teks menjadi tipe datetime. pd.to_datetime() mengubahnya menjadi datetime64[us] — resolusi mikrodetik bawaan pandas 3.0 — dan set_index("tanggal") menjadikannya index baris. Setelah itu data dapat dipilih per periode dengan penulisan singkat: df.loc["2026-02"] mengembalikan seluruh baris Februari, yaitu 81 baris — 84 hari x 3 kota, dikurangi 5 baris hilang, ditambah 2 duplikat bertanggal Februari.
Enam masalah pada tabel di atas menjadi kontrak kerja Bagian 2: duplikat dibuang, nilai yang melanggar batas fisik diperbaiki, kelembapan kosong diisi, dan tujuh tanggal hilang dilengkapi hingga tiap kota utuh 90 hari. Teori tiap teknik sudah dibahas pada section Missing Data dan Outlier Lanjutan serta Data Waktu dan Deret Waktu; di proyek ini kita merangkainya menjadi satu prosedur pembersihan yang tervalidasi.
Sumber bacaan resmi: pandas.read_csv , pandas.to_datetime , dan pandas.DataFrame.set_index .