Disiplin Copy-on-Write

Semantik copy-on-write pandas 3.x dan berakhirnya praktik chained assignment.

Daftar isi

Copy-on-Write adalah semantik memori pandas yang menjadikan setiap hasil seleksi sebagai salinan yang baru mengalokasikan datanya sendiri ketika ditulis, dan pada pandas 3.x semantik ini selalu aktif tanpa pengaturan tambahan. Topik ini memerinci konsekuensinya pada penugasan nilai, mulai dari salinan malas hasil seleksi kolom, pola penulisan dirangkai yang berakhir dengan peringatan, penulisan nilai yang benar melalui loc, hingga posisi metode copy() di aturan baru ini. Setelah membaca, penulisan nilai pada tabel tidak lagi bergantung pada efek samping yang tidak pernah terjadi.

Salinan malas pada hasil seleksi#

Series yang diambil melalui df["kolom"] bukan jendela langsung ke data DataFrame asalnya, melainkan salinan malas yang baru menyalin isinya ketika diubah. Salinan malas berbagi memori dengan asalnya selama belum tersentuh, sehingga pembuatannya murah, tetapi penugasan nilai pertama memicu penyalinan yang berdiri sendiri. Blok berikut menunjukkan konsekuensinya pada kolom suhu_c dataset kontrak.

python Perubahan pada Series hasil seleksi tidak menyentuh DataFrame
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 96
df = pd.DataFrame({
    "waktu": pd.date_range("2026-01-05 07:00", periods=n, freq="15min"),
    "ruang": rng.choice(["Lab A", "Lab B", "Ruang Arsip"], n, p=[0.4, 0.35, 0.25]),
    "suhu_c": np.round(rng.normal(24, 1.5, n), 1),
    "lembap_pct": np.round(rng.normal(60, 5, n), 1),
    "status_sensor": rng.choice(["normal", "kalibrasi", "rusak"], n, p=[0.85, 0.10, 0.05]),
})
df.to_csv("sensor_log.csv", index=False)  # dataset bersama untuk seluruh modul

df = pd.read_csv("sensor_log.csv", parse_dates=["waktu"])
s = df["suhu_c"]
s.iloc[0] = 99.0
print("df tetap:", df["suhu_c"].iloc[0])
print("s berubah:", s.iloc[0])
text Keluaran
df tetap: 21.5
s berubah: 99.0

Penugasan s.iloc[0] = 99.0 sah dan tidak disambut peringatan apa pun, tetapi perubahannya berhenti pada s itu sendiri, sedangkan df tetap menampilkan 21.5 derajat pada baris pertamanya. Isolasi ini berlaku dua arah, karena perubahan pada df setelah seleksi juga tidak merambat ke s yang sudah dibuat. Tidak ada peringatan pada pola ini karena penugasannya sendiri benar, hanya asumsi bahwa perubahannya merambat yang keliru. Dengan begitu, pola apa pun yang mengandalkan perubahan lewat hasil seleksi tidak pernah bekerja pada pandas 3.x.

Chained assignment dan peringatannya#

Penulisan nilai melalui dua tahap seleksi yang dirangkai, yakni chained assignment, tidak pernah mengubah tabel asal pada pandas 3.x dan selalu disambut peringatan ChainedAssignmentError. Ekspresi df["suhu_c"][0] = 99.0 terdiri atas dua langkah yang dijalankan berurutan, yaitu pengambilan kolom yang menghasilkan salinan, lalu penugasan pada salinan itu. Peringatannya berupa warning, bukan exception yang menghentikan program, sehingga perlu ditangkap untuk diperiksa.

python Peringatan ChainedAssignmentError pada pola dirangkai
import warnings
import pandas as pd

df = pd.read_csv("sensor_log.csv", parse_dates=["waktu"])
with warnings.catch_warnings(record=True) as terekam:
    warnings.simplefilter("always")
    df["suhu_c"][0] = 99.0
print("df tetap:", df["suhu_c"].iloc[0])
print("jenis peringatan:", terekam[0].category.__name__)
print(str(terekam[0].message).splitlines()[0])
text Keluaran
df tetap: 21.5
jenis peringatan: ChainedAssignmentError
A value is being set on a copy of a DataFrame or Series through chained assignment.

Nilai 99.0 tidak pernah sampai ke tabel karena yang ditulis adalah salinan sementara hasil langkah pertama, dan peringatan ChainedAssignmentError menyatakan persis hal itu. Pesannya bahkan menyarankan bentuk penulisan yang benar, dan pada topik Seleksi dengan loc pola yang sama sudah diperlihatkan pada kasus seleksi ber-mask. Keluaran yang tercetak tetap bersih karena peringatan itu ditangkap melalui warnings.catch_warnings(record=True), sesuai kebiasaan seluruh topik di jalur ini.

Penulisan nilai yang benar#

Nilai ditulis langsung pada objek DataFrame yang dirujuk, baik melalui df.loc[mask, "kolom"] = nilai untuk baris terpilih maupun df["kolom"] = nilai untuk satu kolom penuh. Kedua bentuk menyasar tabel secara eksplisit dalam satu ekspresi, sehingga tidak ada salinan sementara yang menjadi sasaran penugasan. Blok berikut menerapkan keduanya pada dataset kontrak.

python Penulisan melalui loc dan penugasan satu kolom
import numpy as np
import pandas as pd

df = pd.read_csv("sensor_log.csv", parse_dates=["waktu"])
mask = df["status_sensor"] == "rusak"
df.loc[mask, "suhu_c"] = np.nan
df["selisih_lembap"] = (df["lembap_pct"] - 55).round(1)
print(df.loc[mask, ["ruang", "suhu_c", "status_sensor"]])
print(df[["lembap_pct", "selisih_lembap"]].head(3))
text Keluaran
    ruang  suhu_c status_sensor
43  Lab A     NaN         rusak
87  Lab B     NaN         rusak
89  Lab B     NaN         rusak
   lembap_pct  selisih_lembap
0        68.6            13.6
1        59.1             4.1
2        58.1             3.1

Tiga baris berstatus rusak pada indeks 43, 87, dan 89 kini bertanda NaN pada kolom suhu_c, dan perubahan itu benar-benar tersimpan pada tabel. Penugasan df["selisih_lembap"] = ... membuat kolom baru yang isinya dihitung dari seluruh baris sekaligus tanpa perulangan. Kedua bentuk berjalan tanpa peringatan karena objek sasarannya tidak ambigu, dan kebiasaan inilah yang membuat kode penulisan nilai tidak perlu memanggil copy(). Gunakan bentuk loc setiap kali sasarannya baris tertentu, dan bentuk satu kolom ketika seluruh kolom ikut diganti.

Kapan copy() diperlukan#

Metode copy() bukan lagi alat pengaman tabel asal, melainkan pernyataan maksud bahwa salinan bebas memang dikehendaki. Salinan bebas mengalokasikan datanya seketika dan terisolasi dua arah sejak awal, berbeda dari salinan malas yang menyalinnya hanya ketika ditulis. Blok berikut memperlihatkan isolasi penuh itu.

python Salinan bebas hasil copy() terisolasi dua arah
import pandas as pd

df = pd.read_csv("sensor_log.csv", parse_dates=["waktu"])
salin = df["suhu_c"].copy()
salin.iloc[0] = 40.0
df.loc[0, "suhu_c"] = 30.0
print("df:", df["suhu_c"].iloc[0])
print("salin:", salin.iloc[0])
text Keluaran
df: 30.0
salin: 40.0

Kedua objek kini memuat nilai yang berbeda tanpa saling mengganggu, dan hasil yang sama sebenarnya diperoleh tanpa copy() karena salinan malas pun terisolasi. Memanggil copy() di mana-mana hanya menambah pekerjaan penyalinan tanpa mengubah perilakunya. Gunakan copy() ketika deret akan dimodifikasi lebih dahulu sebelum dipakai, atau ketika deret diteruskan ke fungsi lain yang boleh mengubah isinya tanpa memengaruhi tabel. Pada kasus-kasus itulah penyalinan seketika menyatakan maksud secara jelas.

Kesalahan umum#

Kesalahan yang tersisa adalah mengandalkan efek samping pada deret hasil seleksi, misalnya fungsi yang mengubah argumennya sambil mengharapkan tabel asal ikut berubah. Fungsi semacam itu menerima salinan hasil seleksi, sehingga mutasinya berhenti pada salinan itu dan tabel asalnya tidak pernah tersentuh.

python Fungsi bermutasi tanpa efek samping dan perbaikannya
import pandas as pd

def nolkan_bacaan_pertama(suhu):
    suhu.iloc[0] = 0.0
    return suhu

df = pd.read_csv("sensor_log.csv", parse_dates=["waktu"])
nolkan_bacaan_pertama(df["suhu_c"])
print("setelah pemanggilan biasa:", df["suhu_c"].iloc[0])
df["suhu_c"] = nolkan_bacaan_pertama(df["suhu_c"])
print("setelah penugasan kembali:", df["suhu_c"].iloc[0])
text Keluaran
setelah pemanggilan biasa: 21.5
setelah penugasan kembali: 0.0

Pemanggilan pertama tidak menimbulkan error maupun peringatan, tetapi nilai pada df tetap 21.5 derajat karena fungsi mengubah salinan yang segera dibuang. Perbaikannya adalah fungsi yang mengembalikan Series hasil perubahannya, kemudian hasil itu ditugaskan kembali ke kolom seperti pada pemanggilan kedua yang menyimpan 0.0. Pola fungsi tanpa efek samping semacam ini sekaligus lebih mudah diuji, karena keluarannya diperiksa langsung tanpa membaca tabel asalnya.

Ringkasan#

  • s = df["kolom"] menghasilkan salinan malas, sehingga pengubahan pada s tidak pernah merambat ke df, dan arah sebaliknya pun tidak.
  • Pola dirangkai seperti df["kolom"][0] = 99 memunculkan peringatan ChainedAssignmentError dan nilainya tidak diterapkan pada tabel.
  • Penulisan yang sah adalah df.loc[mask, "kolom"] = nilai untuk baris terpilih dan df["kolom"] = nilai untuk satu kolom penuh.
  • copy() dipakai hanya ketika salinan bebas memang dikehendaki, bukan untuk mengamankan tabel asal.

Referensi#