Ketika sebuah test DataFrame gagal, pertanyaan kita sebenarnya sederhana: apa persisnya yang berbeda? DataFrame.equals() hanya menjawab ya atau tidak, sedangkan pandas.testing.assert_frame_equal() menjawab dengan presisi — baris mana, kolom mana, dtype apa, sampai nilai pertama yang tidak cocok. Utilitas ini adalah standar perbandingan DataFrame di ekosistem pytest dan punya sejumlah opsi yang menentukan kapan dua DataFrame dianggap sama. Chapter ini membedah perilaku bawaannya satu per satu, lengkap dengan kasus toleransi float, dtype, urutan index, dan NaN.

Perilaku Bawaan yang Perlu Dipahami #

assert_frame_equal(kiri, kanan) secara bawaan memeriksa bentuk, urutan index, nama kolom, dtype setiap kolom, dan nilainya. Untuk kolom float, perbandingan nilai memakai toleransi: parameter rtol bawaan 1e-5 (perbedaan relatif hingga 0,001 persen diterima) dan atol bawaan 1e-8. Kombinasi ini masuk akal untuk hasil pembagian atau agregasi float yang jarang persis identik, tetapi bisa menyembunyikan perbedaan kecil yang justru ingin kita tangkap. Script berikut memperagakan setiap perilaku bawaan dan opsi yang mengubahnya — semua dalam satu skrip yang bisa dijalankan berulang.

python
import pandas as pd
from pandas.testing import assert_frame_equal, assert_index_equal, assert_series_equal


def coba(label: str, aksi) -> None:
    """Jalankan satu perbandingan dan laporkan hasilnya dalam satu baris."""
    try:
        aksi()
    except AssertionError as exc:
        print(f"{label}: GAGAL -> {str(exc).splitlines()[0]}")
    else:
        print(f"{label}: lolos")


kiri = pd.DataFrame({"omzet": [1_000.0, 2_000.0]})
selisih_kecil = pd.DataFrame({"omzet": [1_000.00001, 2_000.0]})
selisih_besar = pd.DataFrame({"omzet": [1_100.0, 2_000.0]})

print("--- toleransi bawaan (rtol=1e-5, atol=1e-8) ---")
coba("selisih 0.00001        ", lambda: assert_frame_equal(kiri, selisih_kecil))
coba("selisih 100 (10 persen)", lambda: assert_frame_equal(kiri, selisih_besar))
coba("check_exact=True       ", lambda: assert_frame_equal(kiri, selisih_kecil, check_exact=True))
print()

print("--- pemeriksaan dtype ---")
utuh = pd.DataFrame({"omzet": [1_000, 2_000]})
pecahan = pd.DataFrame({"omzet": [1_000.0, 2_000.0]})
coba("int64 vs float64       ", lambda: assert_frame_equal(utuh, pecahan))
coba("check_dtype=False      ", lambda: assert_frame_equal(utuh, pecahan, check_dtype=False))
print()

print("--- urutan index ---")
acak = pd.DataFrame({"omzet": [2_000.0, 1_000.0]}, index=[1, 0])
coba("urutan index beda      ", lambda: assert_frame_equal(kiri, acak))
coba("check_like=True        ", lambda: assert_frame_equal(kiri, acak, check_like=True))
print()

print("--- NaN dianggap sama jika posisinya sama ---")
dengan_nan = pd.DataFrame({"omzet": [1_000.0, None]})
coba("NaN di posisi sama     ", lambda: assert_frame_equal(dengan_nan, dengan_nan.copy()))
coba("NaN vs angka           ", lambda: assert_frame_equal(dengan_nan, kiri))
print()

print("--- pesan kegagalan lengkap untuk satu kasus ---")
try:
    assert_frame_equal(kiri, selisih_besar)
except AssertionError as exc:
    for baris in str(exc).splitlines():
        print(f"  {baris}")
print()

print("--- varian Series dan Index ---")
coba("assert_series_equal    ", lambda: assert_series_equal(kiri["omzet"], selisih_besar["omzet"]))
coba("assert_index_equal     ", lambda: assert_index_equal(kiri.index, acak.index))
coba("assert_index_equal ok  ", lambda: assert_index_equal(kiri.index, pd.Index([0, 1])))

Output yang Dihasilkan:

text
--- toleransi bawaan (rtol=1e-5, atol=1e-8) ---
selisih 0.00001        : lolos
selisih 100 (10 persen): GAGAL -> DataFrame.iloc[:, 0] (column name="omzet") are different
check_exact=True       : GAGAL -> DataFrame.iloc[:, 0] (column name="omzet") are different

--- pemeriksaan dtype ---
int64 vs float64       : GAGAL -> Attributes of DataFrame.iloc[:, 0] (column name="omzet") are different
check_dtype=False      : lolos

--- urutan index ---
urutan index beda      : GAGAL -> DataFrame.index are different
check_like=True        : lolos

--- NaN dianggap sama jika posisinya sama ---
NaN di posisi sama     : lolos
NaN vs angka           : GAGAL -> DataFrame.iloc[:, 0] (column name="omzet") are different

--- pesan kegagalan lengkap untuk satu kasus ---
  DataFrame.iloc[:, 0] (column name="omzet") are different
  
  DataFrame.iloc[:, 0] (column name="omzet") values are different (50.0 %)
  [index]: [0, 1]
  [left]:  [1000.0, 2000.0]
  [right]: [1100.0, 2000.0]
  At positional index 0, first diff: 1000.0 != 1100.0

--- varian Series dan Index ---
assert_series_equal    : GAGAL -> Series are different
assert_index_equal     : GAGAL -> Index are different
assert_index_equal ok  : lolos

Mari kita bedah bagian pentingnya:

  1. selisih 0.00001: kolom omzet berbeda 0,00001 pada nilai 1.000 — jauh di dalam toleransi rtol=1e-5, sehingga perbandingan lolos. Perbandingan float tanpa toleransi biasanya menghasilkan test yang rapuh terhadap urutan operasi aritmetika.
  2. selisih 100 (10 persen): perbedaan melewati toleransi dan AssertionError terlempar. Baris pertama pesannya menyebut lokasi secara presisi: DataFrame.iloc[:, 0] (column name="omzet") are different.
  3. check_exact=True: selisih kecil 0,00001 yang tadinya lolos kini gagal. Mode ini tepat untuk dtype yang harus persis — integer, string, tanggal — dan untuk kasus saat kita tahu hasilnya memang harus identik bit demi bit.
  4. int64 vs float64: nilai sama tetapi dtype berbeda, dan assert_frame_equal tetap menolaknya karena check_dtype bawaan True. Menambah check_dtype=False membuat perbandingan lolos — opsi yang berguna saat dtype hasil konstruksi memang berbeda dari hasil pembacaan file.
  5. urutan index beda: dua DataFrame dengan baris yang sama tetapi index 0-1 versus 1-0 ditolak, karena urutan index adalah bagian dari identitas. check_like=True menyuruh perbandingan menyamakan urutan index dan kolom terlebih dahulu, sehingga lolos.
  6. NaN di posisi sama: dua NaN pada posisi yang sama dianggap sama oleh utilitas ini — berbeda dari operator == yang menganggap NaN tidak sama dengan apa pun. Sebaliknya NaN dibandingkan dengan angka tetap gagal.
  7. Pesan kegagalan lengkap: output mencetak satu pesan utuh yang memuat persentase sel yang berbeda (50.0 %), nilai [left] dan [right], hingga baris At positional index 0, first diff: 1000.0 != 1100.0. Pesan ini dirancang agar kita tidak perlu mencetak kedua DataFrame secara manual.
  8. assert_series_equal() dan assert_index_equal(): saudara satu keluarga dengan aturan yang sama, untuk membandingkan satu kolom atau satu index saja. Ketiganya dipanggil tanpa pytest pun berjalan, karena hanya berada di modul pandas.testing.

Peta Opsi yang Paling Sering Dipakai #

OpsiBawaanEfek
check_dtypeTrueDtype kolom harus sama; False melewatkan pemeriksaan ini
check_exactFalse untuk float, True untuk dtype eksakMemaksa perbandingan nilai tanpa toleransi
rtol / atol1e-5 / 1e-8Toleransi relatif dan absolut untuk nilai float
check_likeFalseMenyamakan urutan index dan kolom sebelum membandingkan
check_namesTrueNama index dan kolom ikut diperiksa

Membandingkan dua DataFrame adalah alat pemeriksaan di ujung alur kerja; bagian awalnya adalah memastikan data yang masuk memang berbentuk yang dijanjikan. Chapter berikutnya, Validasi Schema Data, membangun pemeriksaan kolom, dtype, dan aturan isi terhadap kontrak yang disepakati sebelum data diproses lebih jauh.

Referensi resmi untuk chapter ini: pandas.testing.assert_frame_equal , pandas.testing.assert_series_equal , dan pandas.testing.assert_index_equal .