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.

python
"""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:

text
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 -> True

Bagian yang paling penting untuk dicermati adalah pembentukan tabel per produk:

  1. groupby("produk", as_index=False): baris dikelompokkan per produk; as_index=False menjadikan produk kolom biasa sehingga tabel hasil mudah dibaca dan diekspor.
  2. Named aggregation pada agg(): satu pemanggilan menghitung empat ukuran sekaligus — transaksi dari count id, terjual dari sum jumlah, omzet dan laba dari sum masing-masing kolom — dengan nama kolom yang kita tentukan sendiri.
  3. sort_values("omzet", ascending=False) dan reset_index(drop=True): tabel diurutkan dari omzet terbesar agar lima baris teratas langsung menjadi jawaban pertanyaan bisnis.
  4. rank(ascending=False, method="min"): kolom peringkat memberi nomor 1 sampai 10 berdasarkan omzet; karena nilai omzet semua produk berbeda, hasilnya bulat dan rapi setelah dikonversi ke int64.
  5. idxmax() dan idxmin(): 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 .