September 28, 2026

Pandas: Mengolah Data Tabular.

Dasar pandas 3.0: DataFrame, membaca CSV dan SQL, filter, groupby, resample time series, merge, dan menangani missing values — dengan contoh data harga beras.

Pandas adalah library untuk data tabular di Python: baca data dari CSV/SQL, bersihkan, agregasi, lalu serahkan ke model atau chart. Di Price Prediction System pandas memegang hampir semua langkah antara API pemerintah dan model forecasting. Catatan ini memakai pandas 3.0 (butuh Python 3.11+).

pip install "pandas>=3.0" sqlalchemy pymysql

Yang berubah di pandas 3.0: Copy-on-Write aktif default (chained assignment tidak lagi mengubah data asli), kolom teks memakai dtype str alih-alih object, alias frekuensi lama seperti "M" dihapus (pakai "ME"), dan tanggal hasil parsing string memakai resolusi mikrodetik (datetime64[us]).


1. Series & DataFrame

Series adalah satu kolom berlabel; DataFrame adalah tabel berisi banyak Series yang berbagi index.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "tanggal": pd.to_datetime(["2025-01-01", "2025-01-02", "2025-01-03",
                               "2025-01-01", "2025-01-02", "2025-01-03"]),
    "kode_kota": ["3507", "3507", "3507", "3578", "3578", "3578"],
    "komoditas": ["Beras Medium"] * 6,
    "harga": [13200, 13250, np.nan, 13800, 13850, 13900],
})

df.info()          # tipe kolom & jumlah non-null
df.head(3)         # 3 baris pertama
df.describe()      # statistik ringkas kolom numerik
df.shape           # (6, 4)
df["harga"].mean() # NaN diabaikan secara default
AksesContohHasil
Satu kolomdf["harga"]Series
Beberapa kolomdf[["tanggal", "harga"]]DataFrame
Labeldf.loc[0, "harga"]nilai
Posisidf.iloc[-1]baris terakhir
Kondisidf.loc[df["harga"] > 13500, "kode_kota"]Series

2. Membaca CSV dan SQL

# CSV: tentukan tipe & parsing tanggal sejak awal
df = pd.read_csv(
    "harga_harian.csv",
    parse_dates=["tanggal"],
    dtype={"kode_kota": "str"},   # kode wilayah jangan jadi angka (buang nol di depan)
)

df.to_csv("export/harga_bersih.csv", index=False)

Untuk database, pandas menerima SQLAlchemy engine/connection. Selalu kirim parameter lewat params, jangan menyusun SQL dengan f-string:

import os
from sqlalchemy import create_engine, text

url = (f"mysql+pymysql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}"
       f"@{os.environ['DB_HOST']}:{os.environ.get('DB_PORT', '3306')}/{os.environ['DB_NAME']}")
engine = create_engine(url, pool_pre_ping=True)

query = text("""
    SELECT tanggal, harga
    FROM history_data_komoditas
    WHERE kode_kota = :kota AND komoditas_id = :kom
      AND tanggal BETWEEN :start AND :end
    ORDER BY tanggal
""")

with engine.connect() as conn:
    harian = pd.read_sql(query, conn, params={
        "kota": "3507", "kom": 27, "start": "2023-01-01", "end": "2025-06-30",
    })

harian["tanggal"] = pd.to_datetime(harian["tanggal"])
harian = harian.set_index("tanggal")

Menulis balik: df.to_sql("nama_tabel", engine, if_exists="append", index=False). Dasar query-nya ada di Script Dasar SQL .


3. Filter & Transformasi Kolom

# Kombinasi kondisi: pakai & | ~ dan tanda kurung
mahal = df[(df["harga"] > 13500) & (df["kode_kota"] == "3578")]

# isin untuk banyak nilai, between untuk rentang
df[df["kode_kota"].isin(["3507", "3578"])]
df[df["tanggal"].between("2025-01-01", "2025-01-02")]

# query() untuk ekspresi yang lebih mudah dibaca
df.query("harga > 13500 and kode_kota == '3578'")

# kolom baru
df = df.assign(
    harga_ribu=df["harga"] / 1000,
    bulan=df["tanggal"].dt.to_period("M"),
)

# transformasi per elemen di kolom teks
df["komoditas"] = df["komoditas"].str.upper()

Mengubah sebagian baris wajib lewat .loc dalam satu langkah:

df.loc[df["harga"] < 0, "harga"] = np.nan    # benar
# df[df["harga"] < 0]["harga"] = np.nan      # chained: tidak berefek di pandas 3

4. Missing Values

Data harga dari API sering bolong (hari libur, pasar tutup, kota tidak melapor).

df["harga"].isna().sum()               # jumlah NaN
df.dropna(subset=["harga"])            # buang baris tanpa harga

# isi dengan rata-rata grupnya (pola di ETL skripsi saya)
df["harga"] = df["harga"].fillna(
    df.groupby(["kode_kota", "komoditas"])["harga"].transform("mean")
)

# untuk time series: bawa nilai terakhir atau interpolasi
s = harian["harga"].ffill()
s = harian["harga"].interpolate(method="time")
MetodeKapan dipakai
dropna()Data cukup banyak dan baris kosong tidak bermakna
fillna(nilai)Ada nilai default yang masuk akal (mis. 0 untuk jumlah)
groupby().transform("mean")Isi dengan rata-rata kelompok yang sama
ffill() / bfill()Harga “tetap sama” sampai ada laporan baru
interpolate(method="time")Series berindeks tanggal dengan jarak tidak rata

5. GroupBy & Agregasi

Polanya split → apply → combine:

ringkas = (
    df.groupby("kode_kota")
      .agg(rata=("harga", "mean"),
           tertinggi=("harga", "max"),
           hari=("harga", "count"))
      .round(0)
      .sort_values("rata", ascending=False)
      .reset_index()
)
print(ringkas)
  • agg dengan named aggregation (nama=("kolom", "fungsi")) menghasilkan nama kolom yang rapi.
  • transform mengembalikan hasil dengan panjang sama seperti data asli — cocok untuk kolom turunan.
  • pct_change() dalam grup: df.groupby("kode_kota")["harga"].pct_change() * 100.

6. Time Series & Resample

Resample butuh index bertipe datetime. Data harian di skripsi diubah menjadi ±30 titik bulanan per kota × jenis beras:

idx = pd.date_range("2023-01-01", "2025-06-30", freq="D")
rng = np.random.default_rng(42)
harian = pd.DataFrame(
    {"harga": 12500 + np.cumsum(rng.normal(3, 25, len(idx))).round()},
    index=idx,
)

bulanan = harian["harga"].resample("ME").agg(["mean", "min", "max", "count"])
print(bulanan.head())
print(len(bulanan))   # 30 bulan

# rolling mean 7 hari & deteksi lonjakan >= 5%
harian["ma7"] = harian["harga"].rolling(7).mean()
harian["pct"] = harian["harga"].pct_change() * 100
lonjakan = harian[harian["pct"].abs() >= 5]
AliasArtiCatatan
"D"harian
"W"mingguan (Minggu)"W-MON" untuk Senin
"MS" / "ME"awal / akhir bulan"M" sudah dihapus di pandas 3
"QE", "YE"akhir kuartal / tahunpengganti "Q", "Y"
"h", "min"jam, menithuruf kecil

Data bulanan inilah yang masuk ke model — lihat Forecasting Harga dengan Exponential Smoothing .


7. Merge & Concat

kota = pd.DataFrame({
    "kode_kota": ["3507", "3578"],
    "nama_kota": ["Kab. Malang", "Kota Surabaya"],
})

gabung = df.merge(kota, on="kode_kota", how="left", validate="many_to_one")

# gabung vertikal beberapa batch hasil fetch
semua = pd.concat([batch_jan, batch_feb], ignore_index=True)   # batch_* = DataFrame dari tiap fetch

# baris yang tidak punya pasangan (pandas 3.0+)
yatim = df.merge(kota, on="kode_kota", how="left_anti")
howHasil
innerhanya key yang ada di kedua sisi
leftsemua baris kiri, kanan diisi NaN bila tidak cocok
outergabungan semua key
left_antibaris kiri yang tidak punya pasangan

Parameter validate melempar error bila relasi tidak sesuai dugaan (mis. key duplikat di tabel master) — murah dan menyelamatkan dari baris yang tiba-tiba berlipat. Konsepnya sama dengan JOIN di Relasi Tabel dan Normalisasi .


Kesalahan Umum

KesalahanAkibatPerbaikan
Chained assignment df[mask]["col"] = xDi pandas 3 tidak mengubah df sama sekali (hanya muncul ChainedAssignmentError warning)df.loc[mask, "col"] = x
resample("M") dari kode lamaValueError di pandas 3Ganti "ME" (atau "MS")
Kode wilayah dibaca sebagai int"0351" jadi 351, merge gagaldtype={"kode": "str"} saat membaca
Lupa pd.to_datetimeresample/.dt error, sort tanggal salahParse tanggal saat baca atau segera setelahnya
iterrows() untuk hitunganSangat lambatOperasi vektor, groupby, atau np.where
Menyusun SQL dengan f-stringSQL injectiontext() + params=
inplace=True dianggap lebih hemat memoriTidak ada jaminan, memutus chainingAssign ulang: df = df.dropna()
Menghitung error setelah normalisasiMetrik tidak bermaknaHitung MAPE/RMSE pada harga rupiah asli

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments