Data yang bersih belum berarti siap analisis. Pertanyaan bisnis jarang jatuh tepat pada kolom mentah — “omzet per produk”, “nilai transaksi terbesar”, dan “nama produk pada laporan penjualan” semuanya adalah turunan dari data mentah. Transformasi data adalah tahap menambah nilai: menghitung field baru, mengelompokkan, mengagregasi, mengurutkan, mengubah bentuk, dan menggabungkan. Semuanya adalah operasi pada list of dictionary — bentuk data yang telah kita pakai sejak chapter Membaca Data CSV.
Data Latihan: Transaksi Penjualan #
Simpan berikut sebagai penjualan.csv — dua belas transaksi selama lima hari:
tanggal,sku,qty,harga
2025-06-13,P001,2,85000
2025-06-13,P005,3,15000
2025-06-14,P002,1,45000
2025-06-14,P001,2,85000
2025-06-14,P008,10,3500
2025-06-15,P001,1,85000
2025-06-15,P003,2,32000
2025-06-15,P005,5,15000
2025-06-16,P002,3,45000
2025-06-16,P004,1,78000
2025-06-16,P001,2,85000
2025-06-17,P005,2,15000Kita baca dengan pola konversi tipe dari chapter sebelumnya:
import csv
def baca_penjualan(nama_berkas):
hasil = []
with open(nama_berkas, newline="", encoding="utf-8") as berkas:
for baris in csv.DictReader(berkas):
hasil.append({
"tanggal": baris["tanggal"],
"sku": baris["sku"],
"qty": int(baris["qty"]),
"harga": int(baris["harga"]),
})
return hasil
penjualan = baca_penjualan("penjualan.csv")
print(f"{len(penjualan)} transaksi")
print(penjualan[0])Output yang Dihasilkan:
12 transaksi
{'tanggal': '2025-06-13', 'sku': 'P001', 'qty': 2, 'harga': 85000}Memetakan dan Menyaring dengan Comprehension #
Operasi transformasi paling dasar adalah map dan filter, dan pada list of records keduanya paling jelas ditulis sebagai list comprehension (chapter List Comprehensions). Menyaring hanya menyembunyikan tanggal — perhatikan bahwa tanggal format ISO membuat perbandingan string bekerja seperti perbandingan tanggal:
akhir_pekan = [p for p in penjualan if p["tanggal"] >= "2025-06-15"]
print(f"Transaksi sejak 15 Juni: {len(akhir_pekan)}")Output yang Dihasilkan:
Transaksi sejak 15 Juni: 7Field Turunan: Diskon dan Nilai Transaksi #
Field turunan dihitung dari field yang ada. Kombinasi {**p, "kolom_baru": nilai} menyalin record lalu menambah kolom — record aslinya tidak tersentuh:
HARGA_DISKON = {"P001": 0.10, "P005": 0.20}
def dengan_diskon(p):
potongan = HARGA_DISKON.get(p["sku"], 0)
harga_akhir = round(p["harga"] * (1 - potongan))
return {**p, "harga_akhir": harga_akhir, "hemat": p["harga"] - harga_akhir}
penjualan_diskon = [dengan_diskon(p) for p in penjualan]
print(penjualan_diskon[0])
print(penjualan_diskon[1])Output yang Dihasilkan:
{'tanggal': '2025-06-13', 'sku': 'P001', 'qty': 2, 'harga': 85000, 'harga_akhir': 76500, 'hemat': 8500}
{'tanggal': '2025-06-13', 'sku': 'P005', 'qty': 3, 'harga': 15000, 'harga_akhir': 12000, 'hemat': 3000}Produk P001 mendapat potongan 10% (85000 menjadi 76500, hemat 8500), sedangkan produk tanpa entri di HARGA_DISKON lolos dengan potongan 0 karena dict.get() mengembalikan nilai bawaan. Field seperti hemat gratis kita dapatkan karena seluruh bahan bakunya sudah ada di record.
Pengelompokan dan Agregasi per Grup #
Pertanyaan “berapa omzet per produk?” membutuhkan dua langkah: kelompokkan baris berdasarkan sku, lalu agregasi tiap kelompok. Pengelompokan standarnya memakai setdefault():
def kelompokkan(baris_list, kolom):
hasil = {}
for baris in baris_list:
hasil.setdefault(baris[kolom], []).append(baris)
return hasil
per_sku = kelompokkan(penjualan, "sku")
ringkasan = {
sku: {
"jumlah": len(rows),
"qty": sum(r["qty"] for r in rows),
"omzet": sum(r["qty"] * r["harga"] for r in rows),
}
for sku, rows in per_sku.items()
}
for sku, info in sorted(ringkasan.items(), key=lambda x: -x[1]["omzet"])[:3]:
print(sku, info)Output yang Dihasilkan:
P001 {'jumlah': 4, 'qty': 7, 'omzet': 595000}
P002 {'jumlah': 2, 'qty': 4, 'omzet': 180000}
P005 {'jumlah': 3, 'qty': 10, 'omzet': 150000}Mari kita bedah inti polanya:
- Baris 5 (
hasil.setdefault(baris[kolom], [])): untuk setiap baris, fungsi mengambil list milik nilai kolomnya — membuat list kosong saat pertama kali — lalu meng-append baris. Hasil akhirnya dictionary{"P001": [baris, ...], "P005": [...]}. - Baris 8-14 (dictionary comprehension): setiap kelompok diringkas menjadi tiga angka: jumlah transaksi (
len), total unit (sumqty), dan omzet (sumqty × harga). Struktur dictionary di dalam comprehension membaca persis seperti rumus bisnisnya. - Baris 15 (
sorted(..., key=lambda x: -x[1]["omzet"])): urutkan berdasarkan omzet menurun lewat nilai negatif;[:3]mengambil tiga teratas.
Perhatikan bahwa omzet P001 (595000) adalah penjumlahan 170000 + 170000 + 85000 + 170000 — angka yang sama akan kita verifikasi ulang di chapter Membangun Data Pipeline.
Mengurutkan Record dan Mengubah Bentuk #
Mengurutkan record memakai sorted() dengan key berbentuk lambda (chapter reversed dan sorted). Karena key adalah fungsi, kita bebas mengurutkan berdasarkan nilai hasil hitung:
terbesar = sorted(penjualan, key=lambda p: p["qty"] * p["harga"], reverse=True)[:3]
for p in terbesar:
total = p["qty"] * p["harga"]
print(f"{p['tanggal']} {p['sku']} Rp{total:,}".replace(",", "."))Output yang Dihasilkan:
2025-06-13 P001 Rp170.000
2025-06-14 P001 Rp170.000
2025-06-16 P001 Rp170.000Kadang alat analisis berikutnya (menggambar grafik, matriks, atau API eksternal) lebih cocok menerima data per kolom daripada per baris. Dictionary comprehension mengubah list of dicts menjadi dict of lists:
kolom = {kunci: [b[kunci] for b in penjualan] for kunci in penjualan[0]}
print(kolom["sku"])
print(f"Total qty: {sum(kolom['qty'])}")Output yang Dihasilkan:
['P001', 'P005', 'P002', 'P001', 'P008', 'P001', 'P003', 'P005', 'P002', 'P004', 'P001', 'P005']
Total qty: 34Setiap kunci diambil dari record pertama, lalu nilainya dipungut dari setiap baris — urutan dan panjang setiap list tetap sejajar, sehingga kolom["sku"][3] dan kolom["qty"][3] masih merujuk transaksi yang sama.
Menggabungkan Dua List Berdasarkan Kunci #
Record penjualan hanya menyimpan sku; laporan yang bisa dibaca manusia butuh nama produk. Data nama tersimpan di file lain, jadi kita gabungkan: bangun dictionary index dari list produk, lalu akses per transaksi:
import csv
def baca_produk(nama_berkas):
hasil = []
with open(nama_berkas, newline="", encoding="utf-8") as berkas:
for baris in csv.DictReader(berkas):
try:
hasil.append({
"id": baris["id"],
"nama": baris["nama"],
"harga": int(baris["harga"]),
"kategori": baris["kategori"],
})
except ValueError:
continue # baris rusak dilewati (pola chapter Membaca Data CSV)
return hasil
produk = baca_produk("produk.csv")
indeks_produk = {p["id"]: p for p in produk}
laporan = [{**p, "nama": indeks_produk[p["sku"]]["nama"]} for p in penjualan]
for baris in laporan[:3]:
print(f"{baris['tanggal']} {baris['sku']} - {baris['nama']}")Output yang Dihasilkan:
2025-06-13 P001 - Kopi Gayo
2025-06-13 P005 - Keripik Singkong
2025-06-14 P002 - Teh MelatiKuncinya ada di baris indeks_produk = {p["id"]: p for p in produk}: dictionary comprehension mengubah pencarian dari O(n) per transaksi menjadi O(1). Pola join ini kita perdalam pada chapter Menggabungkan Dataset, termasuk penanganan kunci yang tidak cocok.
Padanan pandas #
Seluruh operasi di atas punya padanan satu baris di pandas — assign, groupby, sort_values, merge:
import pandas as pd
df = pd.read_csv("penjualan.csv")
df["total"] = df["qty"] * df["harga"]
omzet_sku = df.groupby("sku")["total"].sum().sort_values(ascending=False)
print(omzet_sku.head(3))Output yang Dihasilkan:
sku
P001 595000
P002 180000
P005 150000
Name: total, dtype: int64Angka 595000 untuk P001 sama persis dengan hasil standard library kita — mekanisme dan hasil saling menguatkan. pandas membahas pendalaman transformasi di section Data Science; standard library tetap andalan saat dependensi harus minim atau saat kita perlu mengendalikan setiap langkah secara eksplisit.
Dengan membaca, membersihkan, dan mentransformasi data, tiga tahap utama pengolahan data sudah di tangan. Tugas tersisa memahami cara kerjanya saat skalanya membesar.