Jumlah baris dan total omzet baru menjawab pertanyaan paling dangkal. Pemilik kedai ingin tahu kategori mana yang menopang pendapatan, produk apa yang paling dicari pelanggan, dan seberapa besar transaksi terbesar selama periode laporan. Bagian ini mengubah tumpukan baris valid menjadi ringkasan yang menjawab pertanyaan-pertanyaan tersebut. Berkas sample.csv tidak berubah — kita tetap memakai versi yang mengandung empat baris bermasalah dari Bagian 2, dan seluruh agregasi hanya bekerja pada baris yang lulus validasi.

Memisahkan Baris Valid #

Loop pemilah yang tadinya menempel di dalam main() sekarang kita angkat menjadi fungsi sendiri. main() Bagian 3 membutuhkan hasil pemilahan tersebut sebagai bahan analisis, dan fungsi terpisah membuat alurnya mudah dibaca:

python
def pisahkan_valid(data):
    """Pisahkan baris valid dari baris bermasalah."""
    valid = []
    galat_semua = []
    for nomor, baris in enumerate(data, start=2):
        galat = validasi_baris(baris)
        if galat:
            for pesan in galat:
                galat_semua.append(f"baris {nomor}: {pesan}")
        else:
            valid.append(baris)
    return valid, galat_semua

Isinya sama persis dengan loop Bagian 2; kini fungsi mengembalikan dua nilai sekaligus — list baris valid dan list pesan galat — melalui satu return berisi tuple. Pemanggil menangkap keduanya dengan valid, galat_semua = pisahkan_valid(data).

Akumulasi per Kategori #

Pertanyaan “berapa omzet tiap kategori” dijawab dengan pola akumulasi dictionary: satu dictionary kosong diisi bertahap selagi kita menelusuri baris:

python
def ringkas_per_kategori(barisan):
    """Agregasi transaksi, jumlah item, dan omzet per kategori."""
    ringkasan = {}
    for baris in barisan:
        kategori = baris["kategori"]
        if kategori not in ringkasan:
            ringkasan[kategori] = {"transaksi": 0, "jumlah": 0, "omzet": 0}
        ringkasan[kategori]["transaksi"] += 1
        ringkasan[kategori]["jumlah"] += int(baris["jumlah"])
        ringkasan[kategori]["omzet"] += hitung_omzet(baris)
    return ringkasan

Mari kita bedah polanya:

  1. Baris 3 (ringkasan = {}): dictionary hasil berada di luar loop agar terus terisi lintas baris. Kuncinya nama kategori, nilainya dictionary penghitung.
  2. Baris 6-7 (if kategori not in ringkasan:): kategori yang pertama kali muncul harus diinisialisasi lebih dulu dengan angka nol; tanpa langkah ini, operasi += pada kunci yang belum ada menimbulkan KeyError.
  3. Baris 8-10 (ringkasan[kategori][...] += ...): setiap baris menambah satu transaksi, menambah jumlah item, dan menambah omzet baris ke penghitung kategorinya. Setelah loop selesai, setiap kategori memegang tiga angka yang konsisten.

Untuk data latihan kita, fungsi ini menghasilkan penghitung Minuman dari enam transaksi, Makanan dari dua, dan Camilan dari satu — semuanya berasal dari sembilan baris valid. Fungsi ini murni: input yang sama selalu menghasilkan output yang sama, tanpa menyentuh berkas atau layar. Sifat itu akan memudahkan pengujian pada Bagian 4.

Produk Terlaris dan Transaksi Tertinggi #

Total jumlah per produk mengikuti pola serupa dengan versi lebih ringkas memakai dict.get():

python
def jumlah_per_produk(barisan):
    """Total jumlah item terjual per produk."""
    hasil = {}
    for baris in barisan:
        hasil[baris["produk"]] = hasil.get(baris["produk"], 0) + int(baris["jumlah"])
    return hasil

hasil.get(produk, 0) menggantikan pemeriksaan keanggotaan: kunci yang belum ada otomatis dianggap bernilai 0, sehingga baris 5 merangkap inisialisasi dan penambahan sekaligus.

Dua angka sisa — urutan kategori dan transaksi tertinggi — dihitung dari data yang sudah terkumpul. Mengurutkan dictionary berdasarkan nilainya memakai kombinasi sorted(), items(), dan lambda:

python
>>> ringkasan = {"Minuman": 234000, "Makanan": 49000, "Camilan": 28000}
>>> sorted(ringkasan.items(), key=lambda x: x[1], reverse=True)
[('Minuman', 234000), ('Makanan', 49000), ('Camilan', 28000)]

items() menghasilkan pasangan (kunci, nilai), lalu key=lambda x: x[1] menyuruh sorted() membandingkan elemen kedua tiap pasangan — nilainya. Sementara itu, transaksi tertinggi tidak butuh pengurutan sama sekali:

python
tertinggi = max(valid, key=hitung_omzet)

Fungsi bawaan max() menerima argumen key berupa fungsi lain. Di sini kita mengoper hitung_omzet tanpa tanda kurung — yang dikirim adalah function object itu sendiri, dan max() memanggilnya untuk setiap baris guna membandingkan omzetnya. Menyimpan tertinggi sebagai baris utuh berarti tanggal, produk, dan harganya ikut tersedia untuk ditampilkan.

Menampilkan Ringkasan #

Seluruh angka dirangkai menjadi laporan oleh satu fungsi tampilan, dan main() tinggal memanggilnya:

python
def tampilkan_ringkasan(valid):
    """Cetak laporan ringkasan hasil analisis ke layar."""
    total_omzet = sum(hitung_omzet(baris) for baris in valid)
    rata_rata = total_omzet / len(valid)
    tertinggi = max(valid, key=hitung_omzet)

    print("Ringkasan Penjualan")
    print("===================")
    print(f"Baris valid        : {len(valid)}")
    print(f"Total omzet        : Rp{total_omzet:,}")
    print(f"Rata-rata transaksi: Rp{rata_rata:,.2f}")
    print()

    print("Omzet per Kategori")
    print("------------------")
    print(f"{'Kategori':<12}{'Transaksi':>10}{'Jumlah':>8}{'Omzet':>12}{'Rata-rata':>12}")
    ringkasan = ringkas_per_kategori(valid)
    urutan = sorted(ringkasan.items(), key=lambda x: x[1]["omzet"], reverse=True)
    for kategori, nilai in urutan:
        rata_kategori = nilai["omzet"] / nilai["transaksi"]
        print(
            f"{kategori:<12}"
            f"{nilai['transaksi']:>10}"
            f"{nilai['jumlah']:>8}"
            f"{nilai['omzet']:>12,}"
            f"{rata_kategori:>12,.0f}"
        )
    print()

    print("Produk Terlaris (3 Teratas)")
    print("---------------------------")
    peringkat = sorted(jumlah_per_produk(valid).items(), key=lambda x: (-x[1], x[0]))
    for posisi, (produk, jumlah) in enumerate(peringkat[:3], start=1):
        print(f"{posisi}. {produk:<14} {jumlah} item")
    print()

    print("Transaksi Tertinggi")
    print("-------------------")
    print(
        f"{tertinggi['tanggal']} - {tertinggi['produk']}: "
        f"{tertinggi['jumlah']} x Rp{int(tertinggi['harga']):,} "
        f"= Rp{hitung_omzet(tertinggi):,}"
    )


def main():
    if len(sys.argv) != 2:
        print("Pemakaian: python3 main.py <file.csv>")
        return 1

    nama_file = sys.argv[1]
    try:
        data = baca_data(nama_file)
    except FileNotFoundError:
        print(f"File tidak ditemukan: {nama_file}")
        return 1

    valid, galat_semua = pisahkan_valid(data)

    if galat_semua:
        print("Baris tidak valid (dilewati):")
        for pesan in galat_semua:
            print(f"  - {pesan}")
        print()

    if not valid:
        print("Tidak ada baris valid untuk dianalisis.")
        return 1

    tampilkan_ringkasan(valid)

    return 0

Tiga baris perhitungan di awal tampilkan_ringkasan() menghasilkan seluruh angka utama; sisanya hanyalah pencetakan dengan format yang sudah kita kenal dari Bagian 1. Spesifikasi :,.2f pada rata-rata membulatkan ke dua angka desimal dengan pemisah ribuan, dan :,.0f pada rata-rata kategori membulatkan ke bilangan bulat. Baris if not valid: di main() menjaga pembagian total_omzet / len(valid) agar tidak pernah membagi dengan nol.

Menjalankan Program #

bash
python3 main.py sample.csv

Output yang Dihasilkan:

text
Baris tidak valid (dilewati):
  - baris 5: tanggal tidak berformat YYYY-MM-DD
  - baris 9: nilai jumlah harus lebih besar dari 0
  - baris 11: kolom produk kosong
  - baris 14: kolom harga kosong

Ringkasan Penjualan
===================
Baris valid        : 9
Total omzet        : Rp311,000
Rata-rata transaksi: Rp34,555.56

Omzet per Kategori
------------------
Kategori     Transaksi  Jumlah       Omzet   Rata-rata
Minuman              6      21     234,000      39,000
Makanan              2       3      49,000      24,500
Camilan              1       2      28,000      28,000

Produk Terlaris (3 Teratas)
---------------------------
1. Es Teh Manis   8 item
2. Kopi Susu      5 item
3. Air Mineral    4 item

Transaksi Tertinggi
-------------------
2025-06-06 - Kopi Hitam: 4 x Rp15,000 = Rp60,000

Laporan ini sudah bisa dibaca pemilik kedai: kategori Minuman menopang Rp234,000 dari total Rp311,000, Es Teh Manis adalah produk terlaris dengan 8 item, dan transaksi terbesar adalah 4 cangkir Kopi Hitam senilai Rp60,000. Kode lengkap tahap ini tersedia pada bagian Berkas Sumber di bawah halaman ini. Laporan di layar bagus, tetapi lenyap begitu terminal ditutup — pada Bagian 4 kita menuliskan hasilnya ke berkas dan mengunci logikanya dengan test.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

main.py

Unduh berkas
python
"""Pengolah File CSV — Bagian 3: Mengolah dan meringkas data."""

import csv
import sys
from datetime import datetime

KOLOM_WAJIB = ("tanggal", "produk", "kategori", "jumlah", "harga")


def baca_data(nama_file):
    """Baca seluruh baris file CSV dan kembalikan sebagai list of dict."""
    with open(nama_file, newline="", encoding="utf-8") as berkas:
        return list(csv.DictReader(berkas))


def hitung_omzet(baris):
    """Hitung omzet satu baris: jumlah dikali harga."""
    return int(baris["jumlah"]) * int(baris["harga"])


def validasi_baris(baris):
    """Periksa satu baris data dan kembalikan daftar pesan galat."""
    galat = []

    for kolom in KOLOM_WAJIB:
        nilai = baris.get(kolom)
        if nilai is None or nilai.strip() == "":
            galat.append(f"kolom {kolom} kosong")

    if baris.get("tanggal"):
        try:
            datetime.strptime(baris["tanggal"], "%Y-%m-%d")
        except ValueError:
            galat.append("tanggal tidak berformat YYYY-MM-DD")

    for kolom in ("jumlah", "harga"):
        nilai = baris.get(kolom)
        if nilai is None or nilai.strip() == "":
            continue
        try:
            angka = int(nilai)
        except ValueError:
            galat.append(f"nilai {kolom} bukan angka bulat: {nilai!r}")
        else:
            if angka <= 0:
                galat.append(f"nilai {kolom} harus lebih besar dari 0")

    return galat


def pisahkan_valid(data):
    """Pisahkan baris valid dari baris bermasalah."""
    valid = []
    galat_semua = []
    for nomor, baris in enumerate(data, start=2):
        galat = validasi_baris(baris)
        if galat:
            for pesan in galat:
                galat_semua.append(f"baris {nomor}: {pesan}")
        else:
            valid.append(baris)
    return valid, galat_semua


def ringkas_per_kategori(barisan):
    """Agregasi transaksi, jumlah item, dan omzet per kategori."""
    ringkasan = {}
    for baris in barisan:
        kategori = baris["kategori"]
        if kategori not in ringkasan:
            ringkasan[kategori] = {"transaksi": 0, "jumlah": 0, "omzet": 0}
        ringkasan[kategori]["transaksi"] += 1
        ringkasan[kategori]["jumlah"] += int(baris["jumlah"])
        ringkasan[kategori]["omzet"] += hitung_omzet(baris)
    return ringkasan


def jumlah_per_produk(barisan):
    """Total jumlah item terjual per produk."""
    hasil = {}
    for baris in barisan:
        hasil[baris["produk"]] = hasil.get(baris["produk"], 0) + int(baris["jumlah"])
    return hasil


def tampilkan_ringkasan(valid):
    """Cetak laporan ringkasan hasil analisis ke layar."""
    total_omzet = sum(hitung_omzet(baris) for baris in valid)
    rata_rata = total_omzet / len(valid)
    tertinggi = max(valid, key=hitung_omzet)

    print("Ringkasan Penjualan")
    print("===================")
    print(f"Baris valid        : {len(valid)}")
    print(f"Total omzet        : Rp{total_omzet:,}")
    print(f"Rata-rata transaksi: Rp{rata_rata:,.2f}")
    print()

    print("Omzet per Kategori")
    print("------------------")
    print(f"{'Kategori':<12}{'Transaksi':>10}{'Jumlah':>8}{'Omzet':>12}{'Rata-rata':>12}")
    ringkasan = ringkas_per_kategori(valid)
    urutan = sorted(ringkasan.items(), key=lambda x: x[1]["omzet"], reverse=True)
    for kategori, nilai in urutan:
        rata_kategori = nilai["omzet"] / nilai["transaksi"]
        print(
            f"{kategori:<12}"
            f"{nilai['transaksi']:>10}"
            f"{nilai['jumlah']:>8}"
            f"{nilai['omzet']:>12,}"
            f"{rata_kategori:>12,.0f}"
        )
    print()

    print("Produk Terlaris (3 Teratas)")
    print("---------------------------")
    peringkat = sorted(jumlah_per_produk(valid).items(), key=lambda x: (-x[1], x[0]))
    for posisi, (produk, jumlah) in enumerate(peringkat[:3], start=1):
        print(f"{posisi}. {produk:<14} {jumlah} item")
    print()

    print("Transaksi Tertinggi")
    print("-------------------")
    print(
        f"{tertinggi['tanggal']} - {tertinggi['produk']}: "
        f"{tertinggi['jumlah']} x Rp{int(tertinggi['harga']):,} "
        f"= Rp{hitung_omzet(tertinggi):,}"
    )


def main():
    if len(sys.argv) != 2:
        print("Pemakaian: python3 main.py <file.csv>")
        return 1

    nama_file = sys.argv[1]
    try:
        data = baca_data(nama_file)
    except FileNotFoundError:
        print(f"File tidak ditemukan: {nama_file}")
        return 1

    valid, galat_semua = pisahkan_valid(data)

    if galat_semua:
        print("Baris tidak valid (dilewati):")
        for pesan in galat_semua:
            print(f"  - {pesan}")
        print()

    if not valid:
        print("Tidak ada baris valid untuk dianalisis.")
        return 1

    tampilkan_ringkasan(valid)

    return 0


if __name__ == "__main__":
    sys.exit(main())