Peta dtype pada chapter sebelumnya menunjukkan dua dunia yang berdampingan: dtypes NumPy (int64, float64) yang tidak mengenal missing, dan keluarga nullable pandas (Int64, Float64, boolean, string) yang menuliskan missing sebagai satu objek tunggal bernama pd.NA. Dunia kedua inilah yang paling sering menyelamatkan pekerjaan data sehari-hari, karena kolom bilangan bulat yang memiliki celah tidak lagi dipaksa menjadi float64. Chapter ini mengoperasikan keluarga nullable secara utuh: karakter logika pd.NA, konversi otomatis lewat convert_dtypes(), konversi manual lewat astype(), dan aritmetika yang menjaga missing tetap terlihat.

pd.NA dan Logika Tiga Keadaan #

pd.NA adalah objek tunggal (pd.NA is pd.NA bernilai True) yang dirancang untuk menjalar pada operasi apa pun. Script berikut mengujinya lebih dulu sebelum masuk ke DataFrame.

python
import pandas as pd

# pd.NA: penanda missing yang konsisten di semua dtype nullable
print("pd.NA is pd.NA :", pd.NA is pd.NA)
print("pd.NA == pd.NA :", pd.NA == pd.NA)
print("pd.NA == 1     :", pd.NA == 1)
print("pd.NA | True   :", pd.NA | True)
print("pd.NA | False  :", pd.NA | False)
print()

# convert_dtypes memilih keluarga nullable secara otomatis
df = pd.DataFrame({
    "kode": pd.Series([101, 102, 103], dtype="int64"),
    "stok": pd.Series([12.0, 7.0, 30.0], dtype="float64"),
    "harga": pd.Series([9500.5, 12000.0, 7500.25], dtype="float64"),
    "nama": pd.Series(["Budi", "Siti", "Dewi"], dtype="str"),
    "aktif": pd.Series([True, None, False], dtype="object"),
})
konv = df.convert_dtypes()
for kolom in df.columns:
    print(f"{kolom:6s} {str(df[kolom].dtype):8s} -> {str(konv[kolom].dtype)}")
print()

# astype("Int64"): aman dari int64 dan float bulat
print("float bulat ->", pd.Series([1.0, None, 3.0]).astype("Int64").tolist())
print("str angka   ->", pd.Series(["4", None, "6"]).astype("Int64").tolist())
try:
    pd.Series([1.5, None, 2.9]).astype("Int64")
except TypeError as e:
    print("float desimal ->", type(e).__name__, ":", str(e))
print("round dulu   ->", pd.Series([1.5, None, 2.9]).round().astype("Int64").tolist())
print()

# aritmetika nullable: missing menjalar, tipe hasil menyesuaikan
stok = pd.Series([10, None, 7], dtype="Int64")
print("stok       :", stok.tolist())
print("stok * 2   :", (stok * 2).tolist(), "| dtype:", str((stok * 2).dtype))
print("stok * 2.5 :", (stok * 2.5).tolist(), "| dtype:", str((stok * 2.5).dtype))
print("stok > 8   :", (stok > 8).tolist(), "| dtype:", str((stok > 8).dtype))
print("fillna(0)  :", stok.fillna(0).tolist())
print("sum/mean   :", stok.sum(), "/", stok.mean())
semua_na = pd.Series([pd.NA, pd.NA], dtype="Int64")
print("semua NA   : sum =", semua_na.sum(), "| min_count=1 =", semua_na.sum(min_count=1))

Output yang Dihasilkan:

text
pd.NA is pd.NA : True
pd.NA == pd.NA : <NA>
pd.NA == 1     : <NA>
pd.NA | True   : True
pd.NA | False  : <NA>

kode   int64    -> Int64
stok   float64  -> Int64
harga  float64  -> Float64
nama   str      -> string
aktif  object   -> boolean

float bulat -> [1, <NA>, 3]
str angka   -> [4, <NA>, 6]
float desimal -> TypeError : cannot safely cast non-equivalent object to int64
round dulu   -> [2, <NA>, 3]

stok       : [10, <NA>, 7]
stok * 2   : [20, <NA>, 14] | dtype: Int64
stok * 2.5 : [25.0, <NA>, 17.5] | dtype: Float64
stok > 8   : [True, <NA>, False] | dtype: boolean
fillna(0)  : [10, 0, 7]
sum/mean   : 17 / 8.5
semua NA   : sum = 0 | min_count=1 = <NA>

Mari kita bedah bagian pentingnya:

  1. Baris 4-8 (perilaku pd.NA): pd.NA == pd.NA dan pd.NA == 1 sama-sama menghasilkan <NA>, bukan False. Informasi “nilai ini tidak diketahui” tetap terbawa, berbeda dari nan yang perbandingannya selalu False.
  2. Baris 7 (kombinasi boolean): pd.NA | True bernilai True karena secara logika benar apa pun isi NA-nya, sedangkan pd.NA | False tetap <NA>. Pola tiga keadaan (True, False, NA) ini diwarisi keluarga boolean.
  3. Baris 12-21 (convert_dtypes): method convert_dtypes() memilih dtype nullable yang paling pas untuk tiap kolom — hasilnya Int64, Float64, string, dan boolean.
  4. Baris 25-31 (astype): astype("Int64") berjalan mulus dari int64, float bulat, bahkan string angka; untuk float desimal pandas menolaknya dengan TypeError.
  5. Baris 35-43 (aritmetika): operasi pada Int64 menghasilkan Int64, perkalian dengan 2.5 naik ke Float64, perbandingan menghasilkan boolean — dan <NA> menjalar ke hasil setiap operasi yang bersentuhan dengannya.

Hasil convert_dtypes untuk Lima Kolom #

Blok kedua output memetakan lima dtype awal ke pasangan nullable-nya. Tabel berikut merangkum aturan yang terlihat dari output tersebut.

dtype awalIsi contohHasil convert_dtypes
int64101, 102, 103Int64
float64 (semua bulat)12.0, 7.0, 30.0Int64
float64 (ada desimal)9500.5, 12000.0Float64
strBudi, Siti, Dewistring
object (bool + None)True, None, Falseboolean

Aturan float64 menarik dicermati: convert_dtypes() memeriksa isinya, dan bila semua nilai bulat maka kolom dipilihkan Int64. Method ini tidak mengubah nilai apa pun; ia hanya memilih representasi yang lebih tepat. Karena itu convert_dtypes() adalah langkah pembersihan pertama yang praktis setelah memuat data, sebelum Anda menulis astype() manual untuk kasus khusus.

Konversi Manual dengan astype dan Jalan Keluarnya #

astype("Int64") menerima sumber yang “aman”: int64, float yang semua nilainya bulat, dan string angka — output menunjukkan ["4", None, "6"] berubah menjadi [4, <NA>, 6]. Namun pandas menolak pd.Series([1.5, None, 2.9]).astype("Int64") dengan TypeError: cannot safely cast non-equivalent object to int64; pesan ini muncul karena 1.5 tidak dapat diwakili bilangan bulat tanpa kehilangan informasi. Jalan keluarnya terlihat pada baris round dulu: bulatkan lebih dulu dengan round(), baru astype("Int64") menghasilkan [2, <NA>, 3].

Aritmetika Nullable: Missing Menjalar #

Blok terakhir mengoperasikan Series stok [10, <NA>, 7]. Kalikan dengan 2 maka hasilnya Int64; kalikan dengan 2.5 maka pandas naikkan ke Float64 agar 17.5 tetap utuh; bandingkan dengan 8 maka hasilnya boolean berisi [True, <NA>, False]. Baris fillna(0) menunjukkan cara mengganti missing dengan nilai tetap — 0 — sehingga perhitungan lanjutan tidak lagi menyentuh NA.

Dua baris terakhir membahas ringkasan statistik. stok.sum() menghasilkan 17 karena skipna=True adalah nilai default, dan stok.mean() menghasilkan 8.5 dari dua nilai yang tersisa. Yang lebih halus ada di semua NA: sum() biasa mengembalikan 0 (jumlah dari nol elemen dianggap nol), sedangkan sum(min_count=1) baru mengembalikan <NA> karena jumlah elemen yang ikut dihitung kurang dari satu. Parameter min_count memberi kendali penuh atas definisi “cukup data” untuk setiap agregasi.

Dengan pd.NA, convert_dtypes(), dan aturan aritmetikanya, keluarga nullable siap dipakai pada data produksi. Chapter berikutnya memperkenalkan mesin penyimpanan alternatif untuk dtype-dtype yang sama: Apache Arrow, yang bisa dipanggil cukup dengan dtype_backend="pyarrow" atau nama dtype seperti string[pyarrow] — dan menghemat memori secara nyata pada data teks besar.

Referensi resmi untuk chapter ini: pandas.NA , pandas.DataFrame.convert_dtypes , dan user guide Working with nullable integers .