CSV adalah format pertukaran data paling universal: hasil ekspor Excel, laporan database, hingga data sensor semuanya bisa mendarat sebagai file CSV. Membacanya terasa mudah, tetapi tiga masalah selalu muncul di data nyata: setiap nilai datang sebagai string, ada baris yang rusak, dan format penulisan (delimiter, encoding) tidak selalu standar. Chapter Membaca dan Menulis CSV sudah memperkenalkan API dasar module csv; pada chapter ini kita fokus pada alur kerja data yang sesungguhnya, mulai dari membaca baris sampai menghasilkan agregasi yang bisa dipercaya.

Data Latihan: Daftar Produk #

Sepanjang section ini kita memakai data produk sebuah toko kelontong. Simpan teks berikut sebagai produk.csv:

text
id,nama,harga,kategori,stok
P001,Kopi Gayo,85000,minuman,120
P002,Teh Melati,45000,minuman,200
P003,Gula Aren,32000,bahan pokok,75
P004,"Beras Pandan Wangi, 5kg",78000,bahan pokok,40
P005,Keripik Singkong,15000,camilan,250
P006,Sirup Markisa,22000,minuman,60
P007,Sambal Bawang,28000,camilan,90
P008,Mie Instan Goreng,3500,bahan pokok,500
P009,Susu Kental Manis,14500,minuman,80
P012,Cangkir Keramik,55000,aksesoris,25
P010,Kopi Luwak,Rp 250.000,minuman,10
P011,Kerupuk Udang,,camilan,150

Perhatikan dua jebakan yang sengaja kita tanam. Pertama, nama "Beras Pandan Wangi, 5kg" mengandung koma sehingga wajib dikutip; memecah baris dengan split(",") akan merusak kolomnya, sedangkan module csv memotong dengan benar. Kedua, dua baris terakhir adalah data rusak yang realistis: harga berformat mata uang dan harga kosong.

Membaca Baris sebagai Dictionary #

csv.DictReader menghasilkan satu dictionary per baris, dengan baris pertama file sebagai nama kolom:

python
import csv

with open("produk.csv", newline="", encoding="utf-8") as berkas:
    pembaca = csv.DictReader(berkas)
    for baris in list(pembaca)[:3]:
        print(baris)

Output yang Dihasilkan:

text
{'id': 'P001', 'nama': 'Kopi Gayo', 'harga': '85000', 'kategori': 'minuman', 'stok': '120'}
{'id': 'P002', 'nama': 'Teh Melati', 'harga': '45000', 'kategori': 'minuman', 'stok': '200'}
{'id': 'P003', 'nama': 'Gula Aren', 'harga': '32000', 'kategori': 'bahan pokok', 'stok': '75'}

Mengakses kolom berdasarkan nama jauh lebih aman daripada posisi indeks: urutan kolom boleh berubah tanpa merusak kode. Namun ada satu kebiasaan yang harus kita bongkar sejak awal — semua nilai bertipe str:

python
with open("produk.csv", newline="", encoding="utf-8") as berkas:
    baris_pertama = next(csv.DictReader(berkas))

print(baris_pertama["nama"], "bernilai:", baris_pertama["harga"])
print("Tipe harga:", type(baris_pertama["harga"]).__name__)

Output yang Dihasilkan:

text
Kopi Gayo bernilai: 85000
Tipe harga: str

CSV adalah format teks murni; angka 85000 di file hanyalah karakter. Operasi aritmetika seperti baris["harga"] * 2 tidak akan menghasilkan 170000, melainkan pengulangan string. Konversi tipe adalah tanggung jawab kita.

Konversi Tipe dan Penanganan Baris Rusak #

Karena dua baris terakhir file kita rusak, konversi harus dibungkus try/except per baris agar satu baris buruk tidak menghentikan seluruh proses:

python
import csv

def baca_produk(nama_berkas):
    produk_valid = []
    baris_gagal = []
    with open(nama_berkas, newline="", encoding="utf-8") as berkas:
        for nomor, baris in enumerate(csv.DictReader(berkas), start=2):
            try:
                produk_valid.append({
                    "id": baris["id"],
                    "nama": baris["nama"],
                    "harga": int(baris["harga"]),
                    "kategori": baris["kategori"],
                    "stok": int(baris["stok"]),
                })
            except (ValueError, KeyError) as galat:
                baris_gagal.append((nomor, baris["id"], galat))
    return produk_valid, baris_gagal

produk, gagal = baca_produk("produk.csv")
print(f"Terbaca {len(produk)} baris valid dan {len(gagal)} baris gagal")
for nomor, id_produk, galat in gagal:
    print(f"  Baris {nomor} ({id_produk}): {galat}")

Output yang Dihasilkan:

text
Terbaca 10 baris valid dan 2 baris gagal
  Baris 12 (P010): invalid literal for int() with base 10: 'Rp 250.000'
  Baris 13 (P011): invalid literal for int() with base 10: ''

Mari kita bedah bagian-bagian kuncinya:

  1. Baris 7 (for nomor, baris in enumerate(csv.DictReader(berkas), start=2)): enumerate memberi nomor baris file; start=2 karena baris 1 adalah header, sehingga laporan galat menunjuk nomor baris yang bisa diperiksa langsung di editor.
  2. Baris 9-16 (produk_valid.append({...})): setiap nilai dikonversi eksplisit — int(baris["harga"]) — sebelum disimpan, sehingga sisa program menerima data yang sudah bertipe benar.
  3. Baris 17 (except (ValueError, KeyError) as galat): ValueError menangkap konversi yang gagal (harga "Rp 250.000" atau kosong), KeyError menangkap kolom yang hilang. Baris bermasalah dicatat, bukan dibuang diam-diam.
  4. Baris 18 (baris_gagal.append((nomor, baris["id"], galat))): nomor baris, id produk, dan objek galat disimpan agar bisa ditindaklanjuti setelah proses selesai.

Memisahkan hasil valid dan laporan galat seperti ini adalah pola yang akan kita pakai berulang kali di sepanjang section, sampai versi terbesarnya di chapter Membangun Data Pipeline.

Agregasi Sederhana per Kolom #

Setelah data bertipe benar, pertanyaan analisis seperti “berapa produk per kategori?” atau “berapa nilai persediaan?” tinggal dijawab dengan sum() dan Counter dari chapter collections:

python
from collections import Counter

jumlah_per_kategori = Counter(p["kategori"] for p in produk)
print("Jumlah produk per kategori:")
for kategori, jumlah in sorted(jumlah_per_kategori.items()):
    print(f"  {kategori:<12} {jumlah}")

total_stok = sum(p["stok"] for p in produk)
nilai_persediaan = sum(p["harga"] * p["stok"] for p in produk)
print(f"Total unit stok : {total_stok}")
print(f"Nilai persediaan: Rp{nilai_persediaan:,}".replace(",", "."))

Output yang Dihasilkan:

text
Jumlah produk per kategori:
  aksesoris    1
  bahan pokok  3
  camilan      2
  minuman      4
Total unit stok : 1440
Nilai persediaan: Rp36.595.000

Pola sum(...) dan Counter(...) pada generator expression memproses seluruh list tanpa loop manual — ringkas karena iterasinya terjadi di level C.

Delimiter dan Encoding dari Dunia Nyata #

Dua variasi file CSV sering menjebak pembaca awal. Pertama, Excel dengan pengaturan regional tertentu mengekspor CSV berdelimiter titik koma. Deteksi otomatisnya tersedia lewat csv.Sniffer:

python
import csv

with open("produk_semicolon.csv", newline="", encoding="utf-8") as berkas:
    contoh = berkas.read(1024)
    berkas.seek(0)
    dialek = csv.Sniffer().sniff(contoh, delimiters=",;|\t")
    print(f"Delimiter terdeteksi: {dialek.delimiter!r}")
    for baris in csv.DictReader(berkas, dialect=dialek):
        print(baris["nama"], baris["harga"])

Output yang Dihasilkan:

text
Delimiter terdeteksi: ';'
Kopi Gayo 85000
Teh Melati 45000
Gula Aren 32000
Beras Pandan Wangi, 5kg 78000
Keripik Singkong 15000

Kedua, file “CSV UTF-8” dari Excel diawali byte order mark (BOM). Kalau dibaca dengan encoding="utf-8", BOM menempel di nama kolom pertama:

python
import csv

with open("produk_bom.csv", newline="", encoding="utf-8") as berkas:
    baris_pertama = next(csv.DictReader(berkas))
print(f"Kolom pertama (utf-8)     : {list(baris_pertama)[0]!r}")

with open("produk_bom.csv", newline="", encoding="utf-8-sig") as berkas:
    baris_pertama = next(csv.DictReader(berkas))
print(f"Kolom pertama (utf-8-sig) : {list(baris_pertama)[0]!r}")

Output yang Dihasilkan:

text
Kolom pertama (utf-8)     : '\ufeffid'
Kolom pertama (utf-8-sig) : 'id'

Kolom \ufeffid membuat baris["id"] melempar KeyError yang membingungkan. Encoding utf-8-sig memotong BOM secara otomatis, sehingga baris["id"] bekerja normal.

Kapan Berpindah ke pandas #

Untuk file kecil dan logika sederhana, standard library sudah cukup dan bebas dependensi. pandas unggul saat analisis menjadi interaktif dan berulang. Perhatikan perilakunya pada file kita yang mengandung dua baris rusak:

python
import pandas as pd

df = pd.read_csv("produk.csv")
print(df.dtypes)
print()
df["harga_bersih"] = pd.to_numeric(df["harga"], errors="coerce")
print(df[["id", "harga", "harga_bersih"]].tail(3).to_string(index=False))
print()
print(df.groupby("kategori")["harga_bersih"].mean().round(0).sort_index())

Output yang Dihasilkan:

text
id          object
nama        object
harga       object
kategori    object
stok         int64
dtype: object

  id      harga  harga_bersih
P012      55000       55000.0
P010 Rp 250.000           NaN
P011        NaN           NaN

kategori
aksesoris      55000.0
bahan pokok    37833.0
camilan        21500.0
minuman        41625.0

Satu nilai kotor ("Rp 250.000") membuat kolom harga gagal dinferensi sebagai angka dan seluruh kolom menjadi object. Fungsi pd.to_numeric(errors="coerce") mengubah nilai rusak menjadi NaN, lalu agregasi seperti mean() melewatinya secara otomatis.

SituasiPilihan
Skrip CLI tanpa dependensi, data < puluhan ribu barismodule csv
Eksplorasi interaktif, agregasi berlapis, banyak kolompandas
File sangat besar, melebihi memoristreaming csv (chapter Menangani Data Besar)

pandas dibahas mendalam pada section Data Science, dimulai dari chapter Dasar Pandas. Untuk seluruh section ini kita tetap berpijak pada standard library agar mekanisme di balik layarnya terlihat jelas.