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.

python
"""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:

text
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.

python
"""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:

text
== 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"]): 81

Mari kita telaah bagian-bagian pentingnya:

  1. Blok with tempfile.TemporaryDirectory(): dataset ditulis ke cuaca.csv lalu dimuat ulang dengan pd.read_csv(); di pekerjaan nyata, ganti argumennya dengan lokasi berkas Anda.
  2. df.dtypes: dua kolom teks bertipe str — bawaan pandas 3.0, bukan lagi object seperti pandas 2.x. Empat kolom angka semuanya float64; kelembapan yang seharusnya bilangan bulat ikut menjadi float64 karena sel kosong dibaca sebagai NaN, dan NaN hanya dapat tinggal di kolom float.
  3. describe(): statistik ini memperlihatkan bukti masalah — suhu_max bermaksimum 55.0 derajat dan curah_hujan berminimum -2.10, keduanya mustahil secara fisik. Kolom kelembapan hanya terhitung 258 dari 268 baris, artinya sepuluh nilai hilang.
  4. 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.
MasalahJumlahCara deteksi
Baris duplikat5df.duplicated()
suhu_max di atas 45 derajat3(df["suhu_max"] > 45).sum()
suhu_min > suhu_max2perbandingan dua kolom
curah_hujan negatif4(df["curah_hujan"] < 0).sum()
kelembapan kosong (NaN)10df["kelembapan"].isna()
Tanggal hilang per kota7selisih 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 .