Setiap proyek machine learning dimulai dari data yang berhasil dimuat dengan benar. Seluruh contoh pada Section ini memakai data tabular sintetis — pelanggan, karyawan, dan transaksi — yang sebagian besar berformat CSV atau JSON. Masalah klasiknya: berkas berhasil terbuka, tetapi kolom angka ternyata terbaca sebagai teks karena pemisah atau tanda desimalnya berbeda dari asumsi pandas. Kesalahan kecil pada tahap memuat akan menjalar ke seluruh alur kerja: model menolak data, atau lebih buruk, dilatih pada nilai yang salah tanpa menyampaikan galat apa pun. Chapter ini membahas parameter pd.read_csv yang paling berdampak, varian berkas lokal, round-trip JSON, dan kapan sebaiknya beralih ke format kolom seperti Parquet.

Parameter read_csv yang Sering Dipakai #

Fungsi pd.read_csv memiliki puluhan parameter, tetapi lima di antaranya menyelesaikan sebagian besar masalah nyata di lapangan. Kelima parameter ini bekerja pada tahap parsing — saat teks mentah diubah menjadi DataFrame — sehingga kesalahan di sini tidak bisa diperbaiki oleh model seberapa canggih pun.

ParameterPeranContoh nilai
sepKarakter pemisah kolom"," (default), ";", "\t" untuk TSV
headerBaris yang menjadi nama kolom0 (default), None jika data tanpa header
dtypeMemaksa tipe kolom sejak awal{"usia": "float64"}
na_valuesTeks tambahan yang dianggap hilang["NA", "-", "n/a"]
usecolsHanya membaca kolom terpilih["id_pelanggan", "usia"]

Dua parameter pertama menentukan apakah tabel terpotong benar; tiga sisanya menentukan kualitas DataFrame hasilnya. Parameter na_values layak mendapat perhatian khusus: secara bawaan pandas sudah mengenali NaN, NA, dan string kosong, tetapi data operasional sering memakai penanda lain seperti tanda minus atau teks tidak ada — tanpa na_values, penanda tersebut terbaca sebagai nilai sah.

Pemisah Titik Koma dan Desimal Koma #

Berkas CSV hasil ekspor Excel di banyak lokal Eropa dan Indonesia memakai dua kebiasaan sekaligus: kolom dipisah titik koma dan angka desimal ditulis dengan koma, misalnya 1250000,50. Jika kita membacanya dengan sep=";" saja, nilai 1250000,50 gagal dikenali sebagai angka sehingga seluruh kolom menjadi bertipe object (string). Penjumlahan, rata-rata, dan scaling kemudian menolak bekerja. Solusinya adalah memasangkan sep=";" dengan decimal="," agar parser menukar peran kedua tanda tersebut. Gejala seperti ini paling cepat terlihat dari df.dtypes, jadi biasakan memeriksa tipe kolom segera setelah memuat data.

Membaca dan Menulis JSON #

Format JSON menyimpan data sebagai daftar objek — satu objek per baris — saat kita memakai orient="records". Bentuk ini paling sering dipakai untuk bertukar data dengan layanan web. Kita perlu tahu satu keterbatasan penting: JSON tidak menyimpan informasi tipe 64-bit secara persis, sehingga kolom usia bertipe float64 dengan nilai 34.0 dapat terbaca kembali sebagai int64 karena nilainya bulat. Nilai-nilainya sama, tetapi pembandingan dengan equals() bisa saja menghasilkan False hanya karena beda tipe. Untuk menyimpan DataFrame kerja dalam jangka panjang, format kolom seperti Parquet jauh lebih tepat karena tipe kolom tersimpan bersama datanya.

Contoh Kode: Memuat dan Menyimpan Data #

Contoh berikut memuat data sepuluh pelanggan dari string CSV di memori, memeriksa bentuk dan tipe kolomnya, lalu menyimpan cuplikannya kembali sebagai CSV dan JSON — termasuk membaca ulang JSON untuk membuktikan nilainya utuh.

python
# Memuat dan menyimpan data tabular dengan pandas: CSV (koma dan titik koma),
# JSON orient records, serta penulisan kembali dengan to_csv dan to_json.
# Seluruh "berkas" dibangun sebagai string di memori lewat io.StringIO
# sehingga contoh ini mandiri, tanpa berkas eksternal, dan deterministik.

import io

import pandas as pd

# --- 1) CSV standar: pemisah koma, teks "NA" berarti data hilang ---
csv_koma = """id_pelanggan,nama,kota,usia,saldo_rp
P001,Budi,Jakarta,34,1250000
P002,Siti,Bandung,29,870000
P003,Dewi,Surabaya,41,2310000
P004,Andi,Jakarta,NA,1540000
P005,Rina,Medan,37,980000
P006,Fajar,Bandung,25,640000
P007,Intan,Surabaya,31,1120000
P008,Gita,Jakarta,28,760000
P009,Hadi,Medan,45,2890000
P010,Joko,Bandung,52,3120000
"""

df = pd.read_csv(io.StringIO(csv_koma), na_values=["NA"])
print("== 1) CSV koma ==")
print("shape           :", df.shape)
print("kolom dan tipe  :")
print(df.dtypes.to_string())
print("5 baris pertama :")
print(df.head(5).to_string())

# --- 2) Pemilihan kolom (usecols) dan tipe eksplisit (dtype) ---
df_pilih = pd.read_csv(
    io.StringIO(csv_koma),
    usecols=["id_pelanggan", "kota", "usia"],
    dtype={"usia": "float64"},
    na_values=["NA"],
)
print("\n== 2) usecols + dtype ==")
print("shape           :", df_pilih.shape)
print("kolom dan tipe  :")
print(df_pilih.dtypes.to_string())

# --- 3) CSV lokal Indonesia/Eropa: pemisah titik koma, desimal koma ---
csv_titik_koma = """id_pelanggan;kota;usia;saldo_rp
P001;Jakarta;34;1250000,50
P002;Bandung;29;870000,25
P003;Surabaya;41;2310000,00
P004;Jakarta;28;760000,75
P005;Medan;37;980000,10
"""

df_tanpa_desimal = pd.read_csv(io.StringIO(csv_titik_koma), sep=";")
df_dengan_desimal = pd.read_csv(
    io.StringIO(csv_titik_koma), sep=";", decimal=","
)
print("\n== 3) Pemisah titik koma, desimal koma ==")
print("tanpa decimal=','   -> tipe saldo_rp :", df_tanpa_desimal["saldo_rp"].dtype)
print("dengan decimal=','  -> tipe saldo_rp :", df_dengan_desimal["saldo_rp"].dtype)
print("3 baris pertama hasil pembacaan benar :")
print(df_dengan_desimal.head(3).to_string())

# --- 4) Menyimpan kembali ke CSV ---
csv_keluaran = df.head(3).to_csv(index=False)
print("\n== 4) to_csv(index=False), 3 baris pertama ==")
print(csv_keluaran, end="")

# --- 5) Menyimpan ke JSON orient records lalu membaca ulang ---
json_teks = df.head(3).to_json(orient="records")
print("\n== 5) to_json(orient='records'), 3 baris pertama ==")
print(json_teks)

df_balik = pd.read_json(io.StringIO(json_teks), orient="records")
print("\n== 6) Round-trip JSON -> DataFrame ==")
print("tipe kolom usia di data asal  :", df.head(3)["usia"].dtype)
print("tipe kolom usia setelah balik :", df_balik["usia"].dtype)
nilai_sama = bool((df.head(3).to_numpy() == df_balik.to_numpy()).all())
print("seluruh nilai tersimpan sama  :", nilai_sama)

Output yang Dihasilkan:

text
== 1) CSV koma ==
shape           : (10, 5)
kolom dan tipe  :
id_pelanggan     object
nama             object
kota             object
usia            float64
saldo_rp          int64
5 baris pertama :
  id_pelanggan  nama      kota  usia  saldo_rp
0         P001  Budi   Jakarta  34.0   1250000
1         P002  Siti   Bandung  29.0    870000
2         P003  Dewi  Surabaya  41.0   2310000
3         P004  Andi   Jakarta   NaN   1540000
4         P005  Rina     Medan  37.0    980000

== 2) usecols + dtype ==
shape           : (10, 3)
kolom dan tipe  :
id_pelanggan     object
kota             object
usia            float64

== 3) Pemisah titik koma, desimal koma ==
tanpa decimal=','   -> tipe saldo_rp : object
dengan decimal=','  -> tipe saldo_rp : float64
3 baris pertama hasil pembacaan benar :
  id_pelanggan      kota  usia    saldo_rp
0         P001   Jakarta    34  1250000.50
1         P002   Bandung    29   870000.25
2         P003  Surabaya    41  2310000.00

== 4) to_csv(index=False), 3 baris pertama ==
id_pelanggan,nama,kota,usia,saldo_rp
P001,Budi,Jakarta,34.0,1250000
P002,Siti,Bandung,29.0,870000
P003,Dewi,Surabaya,41.0,2310000

== 5) to_json(orient='records'), 3 baris pertama ==
[{"id_pelanggan":"P001","nama":"Budi","kota":"Jakarta","usia":34.0,"saldo_rp":1250000},{"id_pelanggan":"P002","nama":"Siti","kota":"Bandung","usia":29.0,"saldo_rp":870000},{"id_pelanggan":"P003","nama":"Dewi","kota":"Surabaya","usia":41.0,"saldo_rp":2310000}]

== 6) Round-trip JSON -> DataFrame ==
tipe kolom usia di data asal  : float64
tipe kolom usia setelah balik : int64
seluruh nilai tersimpan sama  : True

Bedah Kode Baris-per-Baris #

  1. Baris 6–8 (import io, import pandas as pd): modul io menyediakan StringIO, objek berperilaku seperti berkas tetapi isinya string di memori. Dengan ini contoh tidak bergantung pada berkas eksternal apa pun, dan kita bisa menyalin-tempel pola yang sama untuk berkas sungguhan dengan pd.read_csv("data/pelanggan.csv").
  2. Baris 10–22 (csv_koma = ...): data sepuluh pelanggan ditulis sebagai string CSV standar. Perhatikan baris P004 yang memuat teks NA pada kolom usia — penanda nilai hilang yang sengaja kita tanam.
  3. Baris 24 (pd.read_csv(io.StringIO(csv_koma), na_values=["NA"])): pembacaan pertama memakai parameter default untuk pemisah, lalu na_values=["NA"] memastikan teks tersebut menjadi NaN, bukan string. Akibatnya kolom usia bertipe float64 — tipe yang mampu merepresentasikan kehilangan nilai — terlihat pada output bagian 1.
  4. Baris 33–38 (df_pilih = pd.read_csv(...)): pembacaan kedua memakai usecols untuk mengambil tiga kolom saja dan dtype untuk memaksa tipe usia. Hasilnya shape menjadi (10, 3); membatasi kolom sejak pembacaan menghemat memori pada berkas besar.
  5. Baris 44–56 (csv_titik_koma dan dua pd.read_csv): berkas varian lokal dipisah titik koma dan memakai koma desimal. Pembacaan pertama (baris 53) tanpa decimal menghasilkan kolom saldo_rp bertipe object — terlihat jelas pada output bagian 3 — sedangkan pembacaan kedua (baris 54–56) dengan sep=";" plus decimal="," menghasilkan float64 dengan nilai 1250000.50.
  6. Baris 64–66 (to_csv(index=False)): menyimpan tiga baris pertama kembali ke format CSV. Parameter index=False mencegah kolom indeks ikut tertulis, sehingga berkas tetap bersih saat dibaca ulang.
  7. Baris 69–78 (to_json dan read_json): to_json(orient="records") menghasilkan daftar objek JSON, satu objek per baris DataFrame. Membaca ulang dengan read_json membuktikan seluruh nilai kembali sama, meskipun tipe kolom usia berubah dari float64 menjadi int64 — keterbatasan format JSON yang sudah kita bahas di atas.

Format Penyimpanan: CSV, JSON, atau Parquet #

Pilihan format sebaiknya mengikuti tujuan penyimpanan. CSV dan JSON bersifat teks sehingga mudah dibaca manusia dan cocok untuk pertukaran antar sistem, tetapi ukurannya besar, tipe kolom tidak tersimpan, dan pembacaannya lambat untuk jutaan baris. Parquet menyimpan data per kolom dalam format biner dengan kompresi dan informasi tipe yang lengkap; pembacaannya jauh lebih cepat dan ukurannya jauh lebih kecil untuk data kerja harian. Untuk memakainya cukup df.to_parquet("data.parquet") dan pd.read_parquet("data.parquet") — kedua fungsi ini membutuhkan pustaka pyarrow yang terpasang terpisah, sehingga pada proyek nyata format ini dipakai mulai dari tahap data kerja, bukan untuk berkas yang dibuka langsung di editor teks.

Data yang sudah termuat dengan tipe yang benar belum berarti data yang sudah dipahami. Langkah berikutnya adalah memeriksa isi, sebaran, dan kualitasnya secara sistematis — materi pada Chapter Eksplorasi Data Awal (EDA).

Referensi Resmi #