Data harian sebuah perusahaan ritel bisa mencapai jutaan baris per hari, dan file histori akhirnya berukuran gigabyte. Memuat file semacam itu seluruhnya ke RAM bukan pilihan. Strateginya ada tiga: memotong data menjadi bagian (chunking), mengalirkan transformasi tanpa parkir data di memori (streaming), dan menggabungkan hasil antara secara bertahap (pola map-reduce). Kita praktikkan ketiganya di Python murni, lalu menutup dengan panduan kapan saatnya berpindah ke pandas, DuckDB, atau Spark.
Tiga Pola Dasar #
Sebelum masuk ke kode, kita petakan dulu peran masing-masing pola:
- Streaming: data dibaca satu baris pada satu waktu dari sumber, diproses, lalu dilupakan — memori hanya menampung satu baris dan hasil agregat. Ini pola
yielddari Chapter Iterators dan Generators. - Chunking: aliran dipotong menjadi blok berukuran tetap, misalnya 25.000 baris. Blok diproses satu per satu; memori puncak mengikuti ukuran blok, bukan ukuran file.
- Map-reduce sederhana: tahap map menormalkan dan menyaring tiap baris menjadi pasangan kunci-nilai; tahap reduce menggabungkan nilai per kunci. Dengan chunking, reduce berjalan dua lapis — parsial per blok, lalu penggabungan akhir antar blok.
Ketiganya digabung dalam satu studi kasus berikut.
Studi Kasus: Agregasi Bertahap dari Iterator Record #
Kita mengolah penjualan.csv berisi 120.000 baris dengan beberapa baris kotor: kolom kota kosong dan total negatif. Targetnya satu laporan: jumlah transaksi dan total pendapatan per kota. Aliran data dipotong lima blok, tiap blok diagregasi parsial, lalu hasil parsial digabung.
import csv
import random
import time
from collections import defaultdict
from itertools import islice
import tracemalloc
N = 120_000
CHUNK = 25_000
# 1. Siapkan sumber data: 120.000 baris dengan beberapa baris kotor
rng = random.Random(7)
kota_pool = ["jakarta", "bandung", "surabaya", "medan", "denpasar", ""]
with open("penjualan.csv", "w", newline="") as f:
tulis = csv.writer(f)
tulis.writerow(["id", "kota", "total"])
for i in range(1, N + 1):
tulis.writerow([i, rng.choice(kota_pool), rng.randrange(-20_000, 250_000, 500)])
# 2. Streaming: iterator record dari file (satu baris di memori pada satu waktu)
def stream_record(nama_file):
with open(nama_file, newline="") as f:
for baris in csv.DictReader(f):
yield baris
# 3. Chunking: potong aliran menjadi blok
def potong_aliran(aliran, ukuran):
while True:
blok = list(islice(aliran, ukuran))
if not blok:
return
yield blok
# 4. Map: normalisasi satu record
def map_bersih(record):
kota = record["kota"].strip().upper()
total = int(record["total"])
if kota == "" or total <= 0:
return None
return (kota, total)
# 5. Reduce: agregasi parsial per blok
def reduce_blok(blok):
parsial = defaultdict(lambda: [0, 0])
for record in blok:
hasil_map = map_bersih(record)
if hasil_map is None:
continue
kota, total = hasil_map
parsial[kota][0] += 1
parsial[kota][1] += total
return dict(parsial)
# 6. Jalankan pipeline bertahap dan gabungkan hasil parsial
tracemalloc.start()
mulai = time.perf_counter()
parsial_list = []
for nomor, blok in enumerate(potong_aliran(stream_record("penjualan.csv"), CHUNK), 1):
parsial_list.append(reduce_blok(blok))
print(f"blok {nomor}: {len(blok)} baris dibaca")
final = defaultdict(lambda: [0, 0])
for parsial in parsial_list:
for kota, (n, s) in parsial.items():
final[kota][0] += n
final[kota][1] += s
durasi = time.perf_counter() - mulai
puncak = tracemalloc.get_traced_memory()[1]
tracemalloc.stop()
final = dict(sorted(final.items()))
print(f"-- hasil akhir ({durasi:.2f} detik) --")
for kota, (n, s) in final.items():
print(f"{kota:10s} {n:6d} transaksi total Rp{s:,}")
print(f"puncak memori: {puncak / 1024:.1f} KB")Output yang Dihasilkan:
blok 1: 25000 baris dibaca
blok 2: 25000 baris dibaca
blok 3: 25000 baris dibaca
blok 4: 25000 baris dibaca
blok 5: 20000 baris dibaca
-- hasil akhir (1.00 detik) --
BANDUNG 18629 transaksi total Rp2,336,736,000
DENPASAR 18366 transaksi total Rp2,299,657,000
JAKARTA 18566 transaksi total Rp2,312,285,000
MEDAN 18286 transaksi total Rp2,295,067,500
SURABAYA 18575 transaksi total Rp2,328,485,000
puncak memori: 15922.6 KBMari kita telaah baris-baris kode di atas:
- Baris 12-18 (pembuatan data): seed tetap
random.Random(7)membuat hasil uji dapat diulang. Kolomkota_poolmemuat string kosong danrandrangememungkinkan total negatif — dua jenis baris kotor yang harus tersaring. - Baris 22-25 (
stream_record): generator pembaca file. Iterator ini adalah sumber aliran; hanya satu baris yang berupa dictionary pada satu waktu. - Baris 29-34 (
potong_aliran):islice(aliran, ukuran)mengambil sampaiukuranitem dari aliran lalu berhenti.list(...)memadatkannya menjadi satu blok; bila blok kosong, generator selesai. Hasilnya: lima blok (25.000 × 4, lalu 20.000 sisa). - Baris 38-43 (
map_bersih): tahap map menormalkan kota ke huruf besar dan membuang baris kotor denganNone. Baris valid menjadi pasangan(kota, total). - Baris 47-56 (
reduce_blok): tahap reduce lapis pertama — agregasi parsial di dalam satu blok.defaultdict(lambda: [0, 0])menyimpan pasangan[jumlah, total]per kota. - Baris 63-72 (penggabungan akhir): reduce lapis kedua menggabungkan lima hasil parsial menjadi
final. Struktur map-reduce-nya:map_bersihdipanggil 120.000 kali,reduce_bloklima kali, penggabungan akhir satu kali.
Kapan Berpindah dari Python Murni #
Python murni unggul pada transformasi per baris yang fleksibel. Namun begitu analitik menjadi rumit — banyak GROUP BY, join, window function — SQL jauh lebih ringkas. Panduan berikut menentukan titik pindahnya:
| Kondisi data | Pilihan yang tepat | Catatan |
|---|---|---|
| File ratusan MB; transformasi baris demi baris | Python stdlib (csv + generator) | Pola chapter ini; tanpa dependensi tambahan |
| Analitik tabular interaktif; data muat di RAM | pandas | DataFrame kaya fitur; memori mengikuti ukuran data |
| Query SQL pada file besar, termasuk melebihi RAM | DuckDB | Satu proses; langsung membaca CSV/Parquet |
| Terabyte; klaster multi-node; batch terjadwal | Spark | Terdistribusi ke banyak mesin; biaya operasional paling tinggi |
Sebagai bukti, laporan yang sama ditulis ulang sebagai satu query DuckDB terhadap file penjualan.csv yang sama.
import duckdb
hasil = duckdb.sql(
"""
SELECT upper(trim(kota)) AS kota,
count(*) AS jumlah,
sum(total) AS total
FROM read_csv_auto('penjualan.csv')
WHERE trim(kota) <> '' AND total > 0
GROUP BY upper(trim(kota))
ORDER BY kota
"""
).fetchall()
for kota, jumlah, total in hasil:
print(f"{kota:10s} {jumlah:6d} transaksi total Rp{total:,}")Output yang Dihasilkan:
BANDUNG 18629 transaksi total Rp2,336,736,000
DENPASAR 18366 transaksi total Rp2,299,657,000
JAKARTA 18566 transaksi total Rp2,312,285,000
MEDAN 18286 transaksi total Rp2,295,067,500
SURABAYA 18575 transaksi total Rp2,328,485,000Angka pada output DuckDB sama persis dengan hasil Python murni — 18.629 transaksi Bandung dengan total Rp2.336.736.000, dan seterusnya untuk lima kota. Sepuluh baris SQL menggantikan seluruh pipeline chunking di atas. Kekuatan Python murni tetap pada kontrol penuh: validasi kustom, panggilan API per baris, atau format yang belum dikenali pembaca SQL.
Rujukan resmi untuk pendalaman: itertools
, dokumentasi pandas
, dan dokumentasi DuckDB
. Pipeline yang andal tidak cukup hanya berjalan — ia harus terbukti benar. Chapter Pengujian Kode Python berikutnya membangun jaring pengamannya dengan pytest.