Dataset bersih 234 baris dari Bagian 2 sudah layak dihitung, dan kini kita sampai pada pertanyaan bisnis yang menjadi alasan proyek ini dibuat: produk mana yang paling menyumbang omzet, kategori mana yang marginnya paling sehat, dan cabang kota mana yang paling kuat. Dengan groupby() dan agg(), ketiga pertanyaan itu terjawab dalam beberapa baris kode. Pada bagian ini kita juga menggabungkan data penjualan dengan tabel harga modal lewat merge() agar bisa menghitung laba, menentukan peringkat produk dengan rank() dan idxmax(), lalu menutup analisis dengan validasi silang. Seluruh polanya berasal dari section GroupBy dan Agregasi serta section Menggabungkan dan Membentuk Data yang kini bekerja pada data sungguhan.
Kolom Turunan dan Gabungan dengan Harga Modal #
Dua kolom turunan pertama dihitung langsung antar kolom: omzet = jumlah * harga adalah uang yang diterima dari penjualan, mengikuti materi Operasi Antar Kolom. Untuk laba, kita perlu satu informasi tambahan yang tidak ada di berkas kasir, yaitu harga beli tiap produk. Informasi itu disediakan fungsi buat_modal() pada buat_dataset.py sebagai DataFrame kecil berisi kolom produk dan harga_modal untuk 10 produk.
Gabungan keduanya dilakukan dengan df.merge(modal, on="produk", how="left", validate="many_to_one"). Relasinya banyak-ke-satu: banyak baris transaksi menunjuk ke satu baris modal per produk, dan argumen validate="many_to_one" memeriksa asumsi itu sejak awal — bila tabel modal ternyata memuat satu produk dua kali, merge langsung menolak bekerja. Setelah merge, kolom modal_beli = jumlah * harga_modal dan laba = omzet - modal_beli melengkapi data. Jumlah baris tetap 234 karena setiap transaksi hanya menemukan satu pasangan harga modal.
Omzet per Produk dengan groupby dan rank #
Skrip berikut memuat data lewat bersihkan(), menyiapkan kolom turunan, lalu menghitung tiga agregasi: per produk, per kategori, dan per kota.
"""Menganalisis kinerja produk dari data penjualan yang sudah bersih."""
from pathlib import Path
import tempfile
import pandas as pd
from buat_dataset import bersihkan, buat_dataset, buat_modal
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as folder:
berkas = Path(folder) / "penjualan.csv"
buat_dataset().to_csv(berkas, index=False)
df = bersihkan(pd.read_csv(berkas))
df["omzet"] = df["jumlah"] * df["harga"]
modal = buat_modal()
df = df.merge(modal, on="produk", how="left", validate="many_to_one")
df["modal_beli"] = df["jumlah"] * df["harga_modal"]
df["laba"] = df["omzet"] - df["modal_beli"]
print(f"data gabungan: {df.shape[0]} baris x {df.shape[1]} kolom")
per_produk = (
df.groupby("produk", as_index=False)
.agg(
transaksi=("id_transaksi", "count"),
terjual=("jumlah", "sum"),
omzet=("omzet", "sum"),
laba=("laba", "sum"),
)
.sort_values("omzet", ascending=False)
.reset_index(drop=True)
)
per_produk["peringkat"] = (
per_produk["omzet"].rank(ascending=False, method="min").astype("int64")
)
print("\n== Lima produk dengan omzet terbesar ==")
print(per_produk.head(5).to_string(index=False))
teratas = per_produk.loc[per_produk["omzet"].idxmax(), "produk"]
terkecil = per_produk.loc[per_produk["omzet"].idxmin(), "produk"]
print(f"\nproduk omzet terbesar (idxmax): {teratas}")
print(f"produk omzet terkecil (idxmin): {terkecil}")
per_kategori = (
df.groupby("kategori", as_index=False)
.agg(omzet=("omzet", "sum"), laba=("laba", "sum"))
.assign(margin_pct=lambda d: (d["laba"] / d["omzet"] * 100).round(1))
.sort_values("omzet", ascending=False)
.reset_index(drop=True)
)
print("\n== Kinerja per kategori ==")
print(per_kategori.to_string(index=False))
per_kota = (
df.groupby("kota", as_index=False)
.agg(transaksi=("id_transaksi", "count"), omzet=("omzet", "sum"))
.sort_values("omzet", ascending=False)
.reset_index(drop=True)
)
print("\n== Kinerja per kota cabang ==")
print(per_kota.to_string(index=False))
total_omzet = int(df["omzet"].sum())
total_cek = int((df["jumlah"] * df["harga"]).sum())
tanpa_modal = set(df["produk"]) - set(modal["produk"])
print("\n== Validasi hasil ==")
print(
f"total omzet {total_omzet} == jumlah x harga {total_cek} "
f"-> {total_omzet == total_cek}"
)
print(f"produk tanpa harga modal: {len(tanpa_modal)}")
print(
f"laba {int(df['laba'].sum())} + modal beli "
f"{int(df['modal_beli'].sum())} == omzet -> "
f"{int(df['laba'].sum() + df['modal_beli'].sum()) == total_omzet}"
)Output yang Dihasilkan:
data gabungan: 234 baris x 11 kolom
== Lima produk dengan omzet terbesar ==
produk transaksi terjual omzet laba peringkat
Beras Premium 5kg 21 109 7412000 1744000 1
Kopi Bubuk 200g 36 195 4680000 1267500 2
Minyak Goreng 2L 19 94 3102000 658000 3
Susu Kental Manis 26 154 1771000 415800 4
Deterjen Bubuk 800g 18 82 1517000 410000 5
produk omzet terbesar (idxmax): Beras Premium 5kg
produk omzet terkecil (idxmin): Shampoo Sachet
== Kinerja per kategori ==
kategori omzet laba margin_pct
Bahan Pokok 13211500 3064500 23.2
Minuman 7695500 2010800 26.1
Kebersihan 1517000 410000 27.0
Perawatan 1243500 363100 29.2
== Kinerja per kota cabang ==
kota transaksi omzet
Surabaya 62 6649500
Yogyakarta 67 6233500
Bandung 48 5963500
Jakarta 57 4821000
== Validasi hasil ==
total omzet 23667500 == jumlah x harga 23667500 -> True
produk tanpa harga modal: 0
laba 5848400 + modal beli 17819100 == omzet -> TrueBagian yang paling penting untuk dicermati adalah pembentukan tabel per produk:
groupby("produk", as_index=False): baris dikelompokkan per produk;as_index=Falsemenjadikanprodukkolom biasa sehingga tabel hasil mudah dibaca dan diekspor.- Named aggregation pada
agg(): satu pemanggilan menghitung empat ukuran sekaligus —transaksidaricountid,terjualdarisumjumlah,omzetdanlabadarisummasing-masing kolom — dengan nama kolom yang kita tentukan sendiri. sort_values("omzet", ascending=False)danreset_index(drop=True): tabel diurutkan dari omzet terbesar agar lima baris teratas langsung menjadi jawaban pertanyaan bisnis.rank(ascending=False, method="min"): kolomperingkatmemberi nomor 1 sampai 10 berdasarkan omzet; karena nilai omzet semua produk berbeda, hasilnya bulat dan rapi setelah dikonversi keint64.idxmax()danidxmin(): cara paling ringkas menemukan baris produk dengan omzet terbesar dan terkecil tanpa perlu melihat seluruh tabel.
Membaca Hasil Agregasi #
Angka pada output memberi beberapa cerita yang tidak tampak dari data mentah. Beras Premium 5kg menduduki peringkat 1 dengan omzet 7412000, disusul Kopi Bubuk 200g dengan omzet 4680000 — meski harganya lebih murah, Kopi Bubuk tercatat pada 36 transaksi, yang terbanyak di dataset, dengan 195 unit terjual. Di ujung lain, Shampoo Sachet menjadi produk dengan omzet terkecil dan idxmin() menemukannya tanpa kita membaca tabel satu per satu.
Perbandingan kategori memperlihatkan pola yang lebih menarik: Bahan Pokok menghasilkan omzet terbesar 13211500 tetapi margin terendah 23,2 persen, sedangkan Perawatan dengan omzet terkecil 1243500 justru mencatat margin tertinggi 29,2 persen. Omzet besar tidak otomatis berarti laba besar — perbedaan harga modal antar produk inilah yang membuat merge pada awal bagian ini bernilai. Sementara itu antar kota, Surabaya memimpin omzet dengan 6649500 meskipun jumlah transaksinya (62) lebih sedikit daripada Yogyakarta (67); transaksi di Surabaya bernilai lebih besar.
Validasi Silang Hasil #
Analisis belum selesai sebelum angkanya dicek. Tiga pemeriksaan di akhir skrip bekerja sebagai gerbang: total omzet yang dihitung dari kolom omzet dibandingkan dengan hitungan langsung jumlah * harga, daftar produk diperiksa tidak ada yang kehilangan pasangan harga modal, dan laba + modal_beli dibandingkan dengan omzet. Ketiganya bernilai cocok dengan total omzet 23667500, sehingga tabel per produk, per kategori, dan per kota dapat dilaporkan dengan yakin.
Dengan kinerja per produk, kategori, dan kota sudah terjawab, sisa pekerjaan proyek tinggal menyusun ringkasan bulanan dan mengekspornya. Bagian 4 menuntaskan langkah terakhir itu dengan resample() dan pivot_table(), lalu merangkai seluruh temuan menjadi narasi singkat untuk pengambil keputusan.
Sumber bacaan resmi: pandas.DataFrame.groupby , pandas.DataFrame.merge , dan user guide GroupBy .