Eksplorasi pada Bagian 6 sudah memetakan medan datanya; sekarang saatnya mengambil jawaban yang tuntas. Pada bagian ini kita kembali ke lima pertanyaan analisis yang dirumuskan pada Bagian 2 dan menjawabnya satu per satu dengan angka dari data bersih 48 baris. Setiap jawaban kita kaitkan langsung ke keputusan bisnis yang menunggu — belanja bahan, jadwal pegawai, dan strategi hari sepi — supaya analisis tidak berhenti sebagai angka semata. Teknik agregasi yang dipakai di sini adalah groupby yang sudah kita kenal pada chapter Analisis Data Kategorikal dan Analisis Data Waktu .

Pertanyaan P1 menanyakan menu penopang pendapatan terbesar beserta kontribusinya, sedangkan P5 meminta jumlah gelas per menu sebagai dasar rencana stok. Keduanya dijawab dari agregasi yang sama: jumlahkan gelas dan pendapatan_ribu per menu, lalu hitung kontribusi persen terhadap total pendapatan dan rata-rata gelas harian:

python
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
jumlah_hari = bersih["tanggal"].nunique()

per_menu = (
    bersih.groupby("menu")
    .agg(gelas_terjual=("gelas", "sum"), pendapatan_ribu=("pendapatan_ribu", "sum"))
    .sort_values("pendapatan_ribu", ascending=False)
)
per_menu["kontribusi_persen"] = (
    100 * per_menu["pendapatan_ribu"] / per_menu["pendapatan_ribu"].sum()
).round(1)
per_menu["rata_gelas_harian"] = (per_menu["gelas_terjual"] / jumlah_hari).round(1)

print(f"Jawaban P1 - performa dan kontribusi tiap menu ({jumlah_hari} hari):")
print(per_menu.to_string())
print(f"\nMenu penopang   : {per_menu.index[0]} "
      f"({per_menu['kontribusi_persen'].iloc[0]}% dari total pendapatan)")
print(f"Menu terkecil   : {per_menu.index[-1]} "
      f"({per_menu['kontribusi_persen'].iloc[-1]}% dari total pendapatan)")
print(f"\nJawaban P5 - kebutuhan stok, rata-rata gelas per hari:")
for menu, baris in per_menu.iterrows():
    print(f"  {menu:<13}: {baris['rata_gelas_harian']:.1f} gelas per hari")

Output yang Dihasilkan:

text
Jawaban P1 - performa dan kontribusi tiap menu (12 hari):
              gelas_terjual  pendapatan_ribu  kontribusi_persen  rata_gelas_harian
menu                                                                              
kopi susu               459            10098               42.7               38.2
matcha latte            291             7275               30.8               24.2
americano               242             4356               18.4               20.2
teh tawar               192             1920                8.1               16.0

Menu penopang   : kopi susu (42.7% dari total pendapatan)
Menu terkecil   : teh tawar (8.1% dari total pendapatan)

Jawaban P5 - kebutuhan stok, rata-rata gelas per hari:
  kopi susu    : 38.2 gelas per hari
  matcha latte : 24.2 gelas per hari
  americano    : 20.2 gelas per hari
  teh tawar    : 16.0 gelas per hari

Mari kita bedah kode dan hasilnya:

  1. .agg(gelas_terjual=("gelas", "sum"), ...): agregasi bernama memberi nama kolom hasil langsung pada satu panggilan, sehingga kita tidak perlu mengganti nama kolom setelahnya. Rincian pola ini ada pada dokumentasi pandas.DataFrame.groupby .
  2. kontribusi_persen: tiap pendapatan menu dibagi total seluruh menu (23.649 ribu) lalu dikali 100. Kopi susu menyumbang 10.098 ribu atau 42,7% — jawaban P1.
  3. rata_gelas_harian: total gelas dibagi jumlah_hari yang dihitung dari data (nunique() menghasilkan 12), bukan diketik manual. Rata-rata ini adalah kebutuhan belanja harian Bu Rina — jawaban P5: sekitar 38 gelas kopi susu, 24 gelas matcha latte, 20 gelas americano, dan 16 gelas teh tawar per hari.
  4. Format {menu:<13}: lebar minimum 13 karakter merapikan kolom menu yang panjangnya berbeda-beda, seperti tampilan tabel pada umumnya.

Hari Terbaik, Hari Tersepi, dan Pola Akhir Pekan #

Pertanyaan P2 dan P3 sama-sama membaca ritme harian, jadi keduanya dijawab dari agregasi per tanggal. P2 mencari nilai maksimum dan minimum, sedangkan P3 membandingkan rata-rata kelompok akhir pekan (Sabtu 10 dan Minggu 11 Januari) dengan kelompok hari kerja:

python
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
AKHIR_PEKAN = ("2026-01-10", "2026-01-11")  # Sabtu dan Minggu
NAMA_HARI = {
    "Monday": "Senin", "Tuesday": "Selasa", "Wednesday": "Rabu",
    "Thursday": "Kamis", "Friday": "Jumat", "Saturday": "Sabtu",
    "Sunday": "Minggu",
}

per_hari = bersih.groupby("tanggal")["pendapatan_ribu"].sum().reset_index()
per_hari["nama_hari"] = [
    NAMA_HARI[pd.to_datetime(t).strftime("%A")] for t in per_hari["tanggal"]
]

terbaik = per_hari.loc[per_hari["pendapatan_ribu"].idxmax()]
tersepi = per_hari.loc[per_hari["pendapatan_ribu"].idxmin()]
print("Jawaban P2 - hari terbaik dan hari tersepi:")
print(f"  Terbaik: {terbaik['nama_hari']}, {terbaik['tanggal']} "
      f"({int(terbaik['pendapatan_ribu'])} ribu)")
print(f"  Tersepi : {tersepi['nama_hari']}, {tersepi['tanggal']} "
      f"({int(tersepi['pendapatan_ribu'])} ribu)")

mask_pekan = per_hari["tanggal"].isin(AKHIR_PEKAN)
rata_pekan = per_hari.loc[mask_pekan, "pendapatan_ribu"].mean()
rata_kerja = per_hari.loc[~mask_pekan, "pendapatan_ribu"].mean()
print("\nJawaban P3 - akhir pekan dibanding hari kerja:")
print(f"  Rata-rata akhir pekan : {rata_pekan:.1f} ribu per hari "
      f"({int(mask_pekan.sum())} hari)")
print(f"  Rata-rata hari kerja  : {rata_kerja:.1f} ribu per hari "
      f"({int((~mask_pekan).sum())} hari)")
print(f"  Akhir pekan {rata_pekan / rata_kerja:.2f} kali hari kerja, "
      f"atau {100 * (rata_pekan / rata_kerja - 1):.1f}% lebih tinggi")

Output yang Dihasilkan:

text
Jawaban P2 - hari terbaik dan hari tersepi:
  Terbaik: Minggu, 2026-01-11 (2915 ribu)
  Tersepi : Rabu, 2026-01-14 (903 ribu)

Jawaban P3 - akhir pekan dibanding hari kerja:
  Rata-rata akhir pekan : 2829.0 ribu per hari (2 hari)
  Rata-rata hari kerja  : 1799.1 ribu per hari (10 hari)
  Akhir pekan 1.57 kali hari kerja, atau 57.2% lebih tinggi

Poin penting dari kode dan hasil tersebut:

  1. idxmax() dan idxmin(): keduanya mengembalikan posisi baris bermilai tertinggi dan terendah, lalu .loc mengambil barisnya. Jawaban P2: hari terbaik adalah Minggu 11 Januari (2.915 ribu) dan hari tersepi Rabu 14 Januari (903 ribu).
  2. Kamus NAMA_HARI: strftime("%A") menghasilkan nama hari Inggris, lalu kamus menerjemahkannya ke Bahasa Indonesia. Menyebut “Minggu 11 Januari” lebih mudah dibaca Bu Rina daripada “2026-01-11”.
  3. Masker isin(AKHIR_PEKAN): satu kolom boolean memisahkan dua kelompok; ~ membaliknya untuk hari kerja. Jawaban P3: akhir pekan rata-rata 2.829,0 ribu per hari — 1,57 kali hari kerja yang hanya 1.799,1 ribu.
  4. Keputusan yang berubah: dengan selisih 57,2%, belanja susu dan penjadwalan pegawai Sabtu-Minggu memang perlu porsi lebih besar. Namun ingat catatan Bagian 6 — kelompok akhir pekan hanya berisi dua hari, jadi rasio ini adalah pola awal yang perlu dipantau beberapa pekan lagi.

Anomali Penurunan pada Rabu 14 Januari #

Pertanyaan P4 berbeda dari P2: kita tidak sekadar mencari nilai terkecil, melainkan hari yang menyimpang jauh dari pola normal. Kita hitung selisih persen tiap hari terhadap rata-rata harian, lalu tandai hari yang jatuh lebih dari 30% di bawah rata-rata. Ambang 30% dipilih sebagai batas praktis — dengan data hanya 12 hari, uji statistik formal belum layak dipakai, jadi kita memakai ambang bisnis yang mudah disepakati:

python
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
AKHIR_PEKAN = ("2026-01-10", "2026-01-11")

per_hari = bersih.groupby("tanggal")["pendapatan_ribu"].sum().reset_index()
rata_semua = per_hari["pendapatan_ribu"].mean()
per_hari["selisih_persen"] = (
    100 * (per_hari["pendapatan_ribu"] - rata_semua) / rata_semua
).round(1)

print(f"Rata-rata pendapatan harian keseluruhan: {rata_semua:.1f} ribu rupiah")
print("\nJawaban P4 - hari dengan selisih di bawah -30% dari rata-rata:")
print(per_hari[per_hari["selisih_persen"] < -30].to_string(index=False))

mask_pekan = per_hari["tanggal"].isin(AKHIR_PEKAN)
rata_kerja = per_hari.loc[~mask_pekan, "pendapatan_ribu"].mean()
anomali = per_hari.loc[per_hari["tanggal"] == "2026-01-14", "pendapatan_ribu"].iloc[0]
rabu_awal = per_hari.loc[per_hari["tanggal"] == "2026-01-07", "pendapatan_ribu"].iloc[0]
print(f"\nPerbandingan hari anomali (Rabu 14 Januari, {int(anomali)} ribu):")
print(f"  terhadap rata-rata semua hari     : {100 * anomali / rata_semua:.1f}%")
print(f"  terhadap rata-rata hari kerja     : {100 * anomali / rata_kerja:.1f}%")
print(f"  terhadap Rabu pekan sebelumnya    : {100 * anomali / rabu_awal:.1f}%")

Output yang Dihasilkan:

text
Rata-rata pendapatan harian keseluruhan: 1970.8 ribu rupiah

Jawaban P4 - hari dengan selisih di bawah -30% dari rata-rata:
   tanggal  pendapatan_ribu  selisih_persen
2026-01-14              903           -54.2

Perbandingan hari anomali (Rabu 14 Januari, 903 ribu):
  terhadap rata-rata semua hari     : 45.8%
  terhadap rata-rata hari kerja     : 50.2%
  terhadap Rabu pekan sebelumnya    : 49.5%

Mari kita baca hasil pemeriksaan anomali tersebut:

  1. Hanya satu hari lolos ambang: dari 12 hari, hanya 14 Januari yang selisihnya -54,2% — jauh melampaui ambang -30%. Hari-hari sepi lain seperti 6 Januari (-20,6%) masih dalam rentang wajar.
  2. Tiga pembanding, satu kesimpulan: 903 ribu itu 45,8% dari rata-rata semua hari, 50,2% dari rata-rata hari kerja, dan 49,5% dari Rabu pekan sebelumnya (1.824 ribu). Bagi mana pun pembandingnya, pendapatan hari itu tinggal sekitar separuh — jawaban P4: ya, ada satu anomali penurunan tajam.
  3. Konteks di luar data: menurut catatan Bu Rina, 14 Januari hujan deras seharian. Data penjualan saja tidak bisa membuktikan penyebab, tetapi konteks ini membuat anomali masuk akal dan sifatnya sekali jalan, bukan perubahan selera pelanggan.

Rangkuman Jawaban Kelima Pertanyaan #

PertanyaanJawaban dari dataKeputusan yang berubah
P1: menu penopang?kopi susu — 10.098 ribu, 42,7% dari pendapatanprioritas belanja susu dan biji kopi
P2: hari terbaik/sepi?terbaik Minggu 11 Januari (2.915 ribu), tersepi Rabu 14 Januari (903 ribu)jadwal pegawai mengikuti ritme harian
P3: akhir pekan vs hari kerja?2.829,0 vs 1.799,1 ribu per hari — rasio 1,57stok dan pegawai ditambah tiap Sabtu-Minggu
P4: ada anomali?satu: Rabu 14 Januari, -54,2% dari rata-ratasiapkan strategi hari hujan
P5: gelas per menu?459 / 291 / 242 / 192 gelas, rata-rata harian 38,2 / 24,2 / 20,2 / 16,0kontrak belanja mingguan dengan pemasok

Kelima pertanyaan dari Bagian 2 kini terjawab habis dengan angka yang bisa direproduksi. Angka-angka ini masih berbentuk tabel teks — padahal Bu Rina perlu melihat polanya sekilas tanpa membaca baris demi baris. Pada Bagian 8: Visualisasi Hasil kita mengubah jawaban-jawaban tadi menjadi grafik batang dan grafik garis yang siap masuk laporan.