Proyek pertama kita menerapkan seluruh alur analisis pada data penjualan sebuah toko ritel fiktif: dari memahami masalah bisnis, mengumpulkan dan memvalidasi data, membersihkan berbagai masalah kualitas, mengeksplorasi, menganalisis performa produk dan waktu, hingga menyusun laporan yang siap dibaca pemangku kepentingan. Kita bekerja untuk satu orang bernama Bu Rina, pemilik kafe Toko Kopi Kita di Bandung, yang selama ini menentukan stok dan jadwal pegawai dari ingatan saja. Sepanjang proyek ini kita menggantikan kebiasaan tersebut dengan keputusan berbasis data.

Dataset yang kita olah adalah hasil ekspor aplikasi kasir selama 12 hari operasional (5-16 Januari 2026) untuk 4 menu: kopi susu, matcha latte, americano, dan teh tawar. Data mentahnya sengaja tidak sempurna — ada baris duplikat, sel yang kosong, nama menu yang tidak konsisten, dan satu nilai pendapatan yang salah — karena itulah kondisi yang paling sering kita temui di lapangan. Seluruh proyek dikerjakan dalam sepuluh bagian berurutan, dan setiap bagian menghasilkan keluaran yang bisa Anda jalankan sendiri.

Alur Sepuluh Bagian #

Kerjakan bagian-bagian ini sesuai urutannya karena setiap tahap dibangun di atas tahap sebelumnya:

BagianFokus tahap
Bagian 1: Memahami MasalahKonteks bisnis Bu Rina, tujuan proyek, dan batasan data
Bagian 2: Menentukan Pertanyaan AnalisisEmpat pertanyaan terukur beserta metrik pendukungnya
Bagian 3: Mengumpulkan DataAsal-usul data ekspor kasir dan struktur kolomnya
Bagian 4: Memvalidasi DataPemeriksaan kualitas: duplikat, sel kosong, nama menu, nilai 0
Bagian 5: Membersihkan DataPerbaikan satu per satu hingga data bersih 48 baris
Bagian 6: Eksplorasi DataStatistik deskriptif, agregasi per menu, per hari, dan akhir pekan
Bagian 7: Analisis PenjualanMenjawab keempat pertanyaan dengan angka
Bagian 8: Visualisasi HasilGrafik batang dan grafik garis untuk laporan
Bagian 9: Menyusun TemuanMemetakan temuan, bukti, dan tindak lanjut
Bagian 10: Menyusun LaporanLaporan akhir siap baca dan refleksi proyek

Materi pendukungnya sudah pernah kita bahas di chapter-chapter sebelumnya: teknik validasi ada pada Section Data Cleaning , alur eksplorasi pada Exploratory Data Analysis , dan prinsip penyusunan ringkasan pada Menyusun Ringkasan Eksekutif .

Dataset Mentah #

Berkas data/penjualan_kotor.csv di bawah ini adalah titik awal seluruh analisis. Kolomnya empat: tanggal (hari penjualan), menu (nama minuman), gelas (jumlah gelas terjual), dan pendapatan_ribu (pendapatan dalam ribuan rupiah). Coba perhatikan sejenak — beberapa baris terlihat janggal, dan justru kejanggalan itulah yang menjadi bahan Bagian 4 dan 5. Tautan unduh tersedia di kepala blok kode berikut:

csv
tanggal,menu,gelas,pendapatan_ribu
2026-01-05,kopi susu,34,748
2026-01-05,matcha latte,21,525
2026-01-05,americano,18,324
2026-01-05,teh tawar,15,150
2026-01-06,kopi susu,31,682
2026-01-06,matcha latte,19,475
2026-01-06,americano,-,288
2026-01-06,teh tawar,12,120
2026-01-07,kopi susu,36,792
2026-01-07,kopi susu,36,792
2026-01-07,matcha latte,22,550
2026-01-07,americano,19,342
2026-01-07,teh tawar,14,140
2026-01-08,Kopi Susu,33,726
2026-01-08,matcha latte,20,500
2026-01-08,americano,17,306
2026-01-08,teh tawar,13,130
2026-01-09,kopi susu,38,836
2026-01-09,matcha latte,24,600
2026-01-09,americano,20,360
2026-01-09,teh tawar,-,160
2026-01-10,kopi susu,52,1144
2026-01-10,matcha latte,35,875
2026-01-10,americano,28,504
2026-01-10,teh tawar,22,220
2026-01-11,kopi susu,55,1210
2026-01-11,matcha latte,37,925
2026-01-11,americano,30,540
2026-01-11,teh tawar,24,240
2026-01-12,kopi susu,40,880
2026-01-12,matcha latte,25,625
2026-01-12,americano,21,378
2026-01-12,teh tawar,17,0
2026-01-13,kopi susu,37,814
2026-01-13,matcha latte,23,575
2026-01-13,americano,19,342
2026-01-13,teh tawar,15,150
2026-01-14,kopi susu,18,396
2026-01-14,matcha latte,11,275
2026-01-14,americano,9,162
2026-01-14,teh tawar,7,70
2026-01-15,kopi susu,41,902
2026-01-15,matcha latte,-,650
2026-01-15,americano,22,396
2026-01-15,teh tawar,18,180
2026-01-16,kopi susu,44,968
2026-01-16,matcha latte,28,700
2026-01-16,americano,23,414
2026-01-16,teh tawar,19,190
2026-01-11,teh tawar,24,240

Skrip Pipeline Utuh #

Seluruh alur proyek dirangkum menjadi satu skrip utuh di src/analisis_penjualan.py: memuat data, memvalidasi, membersihkan, meringkas per menu dan per hari, menulis laporan akhir, lalu mencetak ringkasan. Skrip ini deterministik — hasilnya sama setiap kali dijalankan pada data mentah yang sama. Jalankan dari folder proyek dengan perintah python3 src/analisis_penjualan.py:

python
"""Pipeline analisis penjualan Toko Kopi Kita (periode 5-16 Januari 2026).

Alur lengkap dalam satu skrip yang dapat diulang:
1. Muat data mentah hasil ekspor kasir (data/penjualan_kotor.csv).
2. Validasi kualitas: duplikat, sel kosong "-", nama menu tidak konsisten,
   pendapatan 0.
3. Bersihkan seluruh masalah yang ditemukan.
4. Ringkas penjualan per menu dan per hari (termasuk akhir pekan).
5. Susun temuan utama, tulis laporan/laporan-akhir.txt, cetak ringkasan.

Pemakaian (dari folder proyek):
    python3 src/analisis_penjualan.py
"""

from datetime import datetime
from pathlib import Path

import pandas as pd

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


def muat_data(path=DATA_PATH):
    """Muat CSV mentah hasil ekspor aplikasi kasir."""
    return pd.read_csv(path)


def validasi(df):
    """Kumpulkan temuan masalah kualitas pada data mentah."""
    menu_asli = df["menu"]
    return {
        "jumlah_baris": len(df),
        "duplikat_persis": int(df.duplicated().sum()),
        "sel_gelas_strip": int((df["gelas"] == "-").sum()),
        "menu_tak_konsisten": sorted(
            menu_asli.loc[menu_asli != menu_asli.str.lower()].unique().tolist()
        ),
        "pendapatan_nol": int((df["pendapatan_ribu"] == 0).sum()),
    }


def bersihkan(df):
    """Perbaiki masalah hasil validasi lalu kembalikan data bersih dan catatan perbaikan."""
    bersih = df.drop_duplicates().reset_index(drop=True)
    perbaikan = {"duplikat_dibuang": len(df) - len(bersih)}

    # Sel "-" pada kolom gelas berarti pencatatan kosong.
    tak_konsisten = bersih.loc[
        bersih["menu"] != bersih["menu"].str.lower(), "menu"
    ].unique().tolist()
    bersih["gelas"] = pd.to_numeric(bersih["gelas"].replace("-", pd.NA))
    bersih["menu"] = bersih["menu"].str.lower()
    perbaikan["menu_diseragamkan"] = [
        f"'{nama}' menjadi '{nama.lower()}'" for nama in tak_konsisten
    ]

    # Harga satuan tiap menu diambil dari baris yang lengkap.
    lengkap = bersih[bersih["gelas"].notna() & (bersih["pendapatan_ribu"] > 0)]
    harga_satuan = (
        lengkap["pendapatan_ribu"] / lengkap["gelas"]
    ).groupby(lengkap["menu"]).median()

    # Pendapatan 0 dihitung ulang: gelas dikali harga satuan menu.
    nol = bersih["pendapatan_ribu"] == 0
    nilai_baru = (bersih.loc[nol, "gelas"] * bersih.loc[nol, "menu"].map(harga_satuan)).astype(int)
    perbaikan["pendapatan_diperbaiki"] = [
        f"{int(bersih.loc[idx, 'gelas'])} gelas {bersih.loc[idx, 'menu']} "
        f"menjadi {rupiah(nilai)}"
        for idx, nilai in nilai_baru.items()
    ]
    bersih.loc[nol, "pendapatan_ribu"] = nilai_baru

    # Gelas kosong dihitung dari pendapatan dibagi harga satuan menu.
    kosong = bersih["gelas"].isna()
    perbaikan["gelas_diisi"] = int(kosong.sum())
    bersih.loc[kosong, "gelas"] = (
        bersih.loc[kosong, "pendapatan_ribu"] / bersih.loc[kosong, "menu"].map(harga_satuan)
    )

    bersih["gelas"] = bersih["gelas"].astype(int)
    return bersih, perbaikan


def ringkas_menu(df):
    """Total gelas, pendapatan, dan kontribusi tiap menu."""
    per_menu = (
        df.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)
    return per_menu


def ringkas_harian(df):
    """Total pendapatan tiap tanggal beserta penanda akhir pekan."""
    per_hari = df.groupby("tanggal")["pendapatan_ribu"].sum().reset_index()
    per_hari["akhir_pekan"] = per_hari["tanggal"].isin(AKHIR_PEKAN)
    return per_hari


def hitung_temuan(per_menu, per_hari):
    """Angka kunci yang menjadi isi laporan."""
    total_pendapatan = int(per_menu["pendapatan_ribu"].sum())
    total_gelas = int(per_menu["gelas_terjual"].sum())
    terbaik = per_hari.loc[per_hari["pendapatan_ribu"].idxmax()]
    kedua = per_hari.nlargest(2, "pendapatan_ribu").iloc[1]
    tersepi = per_hari.loc[per_hari["pendapatan_ribu"].idxmin()]
    akhir_pekan = per_hari[per_hari["akhir_pekan"]]
    hari_kerja = per_hari[~per_hari["akhir_pekan"]]
    return {
        "total_pendapatan_ribu": total_pendapatan,
        "total_gelas": total_gelas,
        "rata_harian_ribu": total_pendapatan / len(per_hari),
        "menu_terlaris": per_menu.index[0],
        "kontribusi_terlaris": float(per_menu["kontribusi_persen"].iloc[0]),
        "gelas_terlaris": int(per_menu["gelas_terjual"].iloc[0]),
        "pendapatan_terlaris_ribu": int(per_menu["pendapatan_ribu"].iloc[0]),
        "menu_terkecil": per_menu.index[-1],
        "kontribusi_terkecil": float(per_menu["kontribusi_persen"].iloc[-1]),
        "hari_terbaik": str(terbaik["tanggal"]),
        "pendapatan_terbaik_ribu": int(terbaik["pendapatan_ribu"]),
        "hari_kedua": str(kedua["tanggal"]),
        "pendapatan_kedua_ribu": int(kedua["pendapatan_ribu"]),
        "hari_tersepi": str(tersepi["tanggal"]),
        "pendapatan_tersepi_ribu": int(tersepi["pendapatan_ribu"]),
        "persen_tersepi_dari_rata": 100 * int(tersepi["pendapatan_ribu"]) / (total_pendapatan / len(per_hari)),
        "rata_akhir_pekan_ribu": float(akhir_pekan["pendapatan_ribu"].mean()),
        "rata_hari_kerja_ribu": float(hari_kerja["pendapatan_ribu"].mean()),
    }


def label_tanggal(iso):
    """Ubah '2026-01-11' menjadi 'Minggu 11 Januari' (tanpa jam, tetap deterministik)."""
    tanggal = datetime.strptime(iso, "%Y-%m-%d")
    return f"{NAMA_HARI[tanggal.strftime('%A')]} {tanggal.day} Januari"


def ribuan_id(nilai):
    """Format bilangan bulat dengan titik ribuan gaya Indonesia."""
    return f"{int(nilai):,}".replace(",", ".")


def desimal_id(nilai, digit=2):
    """Format desimal dengan koma gaya Indonesia, mis. '1,57'."""
    return f"{nilai:.{digit}f}".replace(".", ",")


def rupiah(ribu):
    """Format angka ribu rupiah menjadi 'Rp 23.649.000'."""
    return "Rp " + ribuan_id(round(ribu * 1000))


def persen_id(nilai):
    """Format angka menjadi '42,7%' (gaya Indonesia, koma desimal)."""
    return desimal_id(nilai, 1) + "%"


def tulis_laporan(temuan, temuan_validasi, perbaikan, jumlah_bersih):
    """Susun laporan akhir lalu tulis ke laporan/laporan-akhir.txt."""
    rasio_pekan = temuan["rata_akhir_pekan_ribu"] / temuan["rata_hari_kerja_ribu"]
    naik_persen = 100 * (rasio_pekan - 1)
    baris = []
    baris.append("=" * 64)
    baris.append("LAPORAN ANALISIS PENJUALAN - TOKO KOPI KITA")
    baris.append("=" * 64)
    baris.append("Periode data : 5-16 Januari 2026 (12 hari, dua pekan penuh)")
    baris.append("Pemilik      : Bu Rina")
    baris.append("Sumber data  : ekspor aplikasi kasir (data/penjualan_kotor.csv)")
    baris.append("Pengolah     : src/analisis_penjualan.py (pandas)")
    baris.append("")
    baris.append("1. RINGKASAN EKSEKUTIF")
    baris.append("-" * 64)
    baris.append(
        f"Sepanjang 5-16 Januari 2026, Toko Kopi Kita membukukan pendapatan\n"
        f"total {rupiah(temuan['total_pendapatan_ribu'])} dari "
        f"{ribuan_id(temuan['total_gelas'])} gelas terjual."
    )
    baris.append(
        f"{temuan['menu_terlaris']} menjadi menu penopang dengan kontribusi\n"
        f"{persen_id(temuan['kontribusi_terlaris'])} dari total pendapatan."
    )
    baris.append(
        f"Rata-rata pendapatan akhir pekan {rupiah(temuan['rata_akhir_pekan_ribu'])}\n"
        f"per hari, {persen_id(naik_persen)} di atas rata-rata hari kerja\n"
        f"{rupiah(temuan['rata_hari_kerja_ribu'])}."
    )
    baris.append(
        f"{label_tanggal(temuan['hari_tersepi'])} menjadi hari tersepi\n"
        f"({rupiah(temuan['pendapatan_tersepi_ribu'])}, "
        f"{persen_id(temuan['persen_tersepi_dari_rata'])} dari rata-rata harian)\n"
        f"setelah hujan deras seharian menurut catatan Bu Rina."
    )
    baris.append("")
    baris.append("2. TEMUAN UTAMA")
    baris.append("-" * 64)
    baris.append(
        f"T1. {temuan['menu_terlaris']} adalah menu penopang.\n"
        f"    {ribuan_id(temuan['gelas_terlaris'])} gelas terjual, pendapatan "
        f"{rupiah(temuan['pendapatan_terlaris_ribu'])}\n"
        f"    ({persen_id(temuan['kontribusi_terlaris'])} dari total). Stok susu dan\n"
        f"    biji kopi wajib diprioritaskan setiap hari."
    )
    baris.append(
        f"T2. Akhir pekan adalah periode tersibuk.\n"
        f"    {label_tanggal(temuan['hari_kedua'])} {rupiah(temuan['pendapatan_kedua_ribu'])} dan\n"
        f"    {label_tanggal(temuan['hari_terbaik'])} {rupiah(temuan['pendapatan_terbaik_ribu'])}\n"
        f"    (tertinggi sepanjang periode). Rata-rata akhir pekan "
        f"{desimal_id(rasio_pekan)} kali\n"
        f"    rata-rata hari kerja."
    )
    baris.append(
        f"T3. {label_tanggal(temuan['hari_tersepi'])} adalah anomali penurunan.\n"
        f"    Pendapatan {rupiah(temuan['pendapatan_tersepi_ribu'])}, hanya "
        f"{persen_id(temuan['persen_tersepi_dari_rata'])}\n"
        f"    dari rata-rata harian. Penyebabnya hujan deras seharian, bukan\n"
        f"    perubahan selera pelanggan."
    )
    baris.append(
        f"T4. {temuan['menu_terkecil']} menyumbang porsi terkecil.\n"
        f"    Kontribusinya {persen_id(temuan['kontribusi_terkecil'])} dari total\n"
        f"    pendapatan. Menu ini tetap dipertahankan sebagai pilihan non-kopi\n"
        f"    yang murah."
    )
    baris.append("")
    baris.append("3. ANGKA KUNCI")
    baris.append("-" * 64)
    baris.append(f"- Pendapatan total 12 hari   : {rupiah(temuan['total_pendapatan_ribu'])}")
    baris.append(f"- Rata-rata pendapatan harian: {rupiah(temuan['rata_harian_ribu'])}")
    baris.append(f"- Gelas terjual              : {ribuan_id(temuan['total_gelas'])} gelas")
    baris.append(f"- Hari terbaik               : {temuan['hari_terbaik']} ({rupiah(temuan['pendapatan_terbaik_ribu'])})")
    baris.append(f"- Hari tersepi               : {temuan['hari_tersepi']} ({rupiah(temuan['pendapatan_tersepi_ribu'])})")
    baris.append(f"- Rata-rata akhir pekan      : {rupiah(temuan['rata_akhir_pekan_ribu'])} per hari")
    baris.append(f"- Rata-rata hari kerja       : {rupiah(temuan['rata_hari_kerja_ribu'])} per hari")
    baris.append("")
    baris.append("4. REKOMENDASI")
    baris.append("-" * 64)
    baris.append(
        "R1. Tambah porsi stok susu dan biji kopi jelang akhir pekan;\n"
        f"    belanja Sabtu-Minggu perlu sekitar {persen_id(naik_persen)} lebih\n"
        f"    banyak dari hari kerja."
    )
    baris.append(
        "R2. Siapkan strategi hari hujan: promo minuman hangat atau layanan\n"
        f"    pesan-antar agar hari seperti {temuan['hari_tersepi']} tidak\n"
        f"    sepenuhnya sepi."
    )
    baris.append(
        f"R3. Pertahankan {temuan['menu_terkecil']} sebagai menu pelengkap tanpa\n"
        f"    menambah stok besar; pantau kontribusinya lagi setelah tiga bulan."
    )
    baris.append(
        f"R4. Perbaiki kebiasaan input kasir: data mentah memuat\n"
        f"    {perbaikan['duplikat_dibuang']} baris duplikat, "
        f"{perbaikan['gelas_diisi']} sel gelas kosong,\n"
        f"    {len(perbaikan['menu_diseragamkan'])} nama menu tidak konsisten, dan\n"
        f"    {temuan_validasi['pendapatan_nol']} pendapatan 0 dari total\n"
        f"    {temuan_validasi['jumlah_baris']} baris yang diekspor."
    )
    baris.append("")
    baris.append("5. CATATAN KUALITAS DATA")
    baris.append("-" * 64)
    baris.append(f"- Baris data mentah : {temuan_validasi['jumlah_baris']}")
    baris.append(f"- Baris data bersih : {jumlah_bersih}")
    baris.append(
        f"- Pembersihan       : {perbaikan['duplikat_dibuang']} duplikat persis dibuang,\n"
        f"                     {perbaikan['gelas_diisi']} sel gelas '-' diisi ulang dari\n"
        f"                     harga satuan menu, "
        f"{' dan '.join(perbaikan['menu_diseragamkan'])},\n"
        f"                     1 pendapatan 0 dihitung ulang\n"
        f"                     ({perbaikan['pendapatan_diperbaiki'][0]})."
    )
    baris.append("- Seluruh angka pada laporan ini dapat direproduksi dengan")
    baris.append("  menjalankan ulang skrip yang sama pada data mentah yang sama.")
    baris.append("=" * 64)

    Path(LAPORAN_PATH).parent.mkdir(parents=True, exist_ok=True)
    with open(LAPORAN_PATH, "w", encoding="utf-8") as berkas:
        berkas.write("\n".join(baris) + "\n")
    return "\n".join(baris)


def main():
    print("== Pipeline Analisis Penjualan - Toko Kopi Kita ==")
    mentah = muat_data()
    print(f"Muat data mentah   : {DATA_PATH} ({len(mentah)} baris)")

    print("\nValidasi data mentah:")
    temuan_validasi = validasi(mentah)
    print(f"  - baris duplikat persis  : {temuan_validasi['duplikat_persis']}")
    print(f"  - sel gelas bernilai '-' : {temuan_validasi['sel_gelas_strip']}")
    print(f"  - nama menu tak konsisten: {temuan_validasi['menu_tak_konsisten']}")
    print(f"  - pendapatan tercatat 0  : {temuan_validasi['pendapatan_nol']}")

    bersih, perbaikan = bersihkan(mentah)
    print(f"\nPembersihan selesai: {len(bersih)} baris bersih, {bersih['menu'].nunique()} menu unik")

    per_menu = ringkas_menu(bersih)
    print("\nRingkasan per menu:")
    print(per_menu.to_string())

    per_hari = ringkas_harian(bersih)
    temuan = hitung_temuan(per_menu, per_hari)
    print("\nRingkasan harian:")
    print(f"  - total pendapatan 12 hari : {temuan['total_pendapatan_ribu']} ribu")
    print(f"  - hari terbaik             : {temuan['hari_terbaik']} ({temuan['pendapatan_terbaik_ribu']} ribu)")
    print(f"  - hari tersepi             : {temuan['hari_tersepi']} ({temuan['pendapatan_tersepi_ribu']} ribu)")
    print(f"  - rata-rata akhir pekan    : {temuan['rata_akhir_pekan_ribu']:.1f} ribu")
    print(f"  - rata-rata hari kerja     : {temuan['rata_hari_kerja_ribu']:.1f} ribu")

    tulis_laporan(temuan, temuan_validasi, perbaikan, len(bersih))
    print(f"\nLaporan ditulis ke: {LAPORAN_PATH}")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
== Pipeline Analisis Penjualan - Toko Kopi Kita ==
Muat data mentah   : data/penjualan_kotor.csv (50 baris)

Validasi data mentah:
  - baris duplikat persis  : 2
  - sel gelas bernilai '-' : 3
  - nama menu tak konsisten: ['Kopi Susu']
  - pendapatan tercatat 0  : 1

Pembersihan selesai: 48 baris bersih, 4 menu unik

Ringkasan per menu:
              gelas_terjual  pendapatan_ribu  kontribusi_persen
menu                                                           
kopi susu               459            10098               42.7
matcha latte            291             7275               30.8
americano               242             4356               18.4
teh tawar               192             1920                8.1

Ringkasan harian:
  - total pendapatan 12 hari : 23649 ribu
  - hari terbaik             : 2026-01-11 (2915 ribu)
  - hari tersepi             : 2026-01-14 (903 ribu)
  - rata-rata akhir pekan    : 2829.0 ribu
  - rata-rata hari kerja     : 1799.1 ribu

Laporan ditulis ke: laporan/laporan-akhir.txt

Dari ringkasan tersebut terlihat cerita utuhnya: kopi susu menopang 42,7% pendapatan, akhir pekan jauh lebih ramai daripada hari kerja, dan tanggal 14 Januari tercatat sebagai hari tersepi. Bagian-bagian berikutnya membedah dari mana angka-angka itu berasal, langkah demi langkah.

Laporan Akhir #

Skrip di atas juga menulis laporan/laporan-akhir.txt — laporan datar yang siap dibaca Bu Rina tanpa perlu membuka Python. Isinya lima bagian: ringkasan eksekutif, temuan utama, angka kunci, rekomendasi, dan catatan kualitas data. Anda bisa mengunduhnya melalui tautan pada kepala blok berikut:

text
================================================================
LAPORAN ANALISIS PENJUALAN - TOKO KOPI KITA
================================================================
Periode data : 5-16 Januari 2026 (12 hari, dua pekan penuh)
Pemilik      : Bu Rina
Sumber data  : ekspor aplikasi kasir (data/penjualan_kotor.csv)
Pengolah     : src/analisis_penjualan.py (pandas)

1. RINGKASAN EKSEKUTIF
----------------------------------------------------------------
Sepanjang 5-16 Januari 2026, Toko Kopi Kita membukukan pendapatan
total Rp 23.649.000 dari 1.184 gelas terjual.
kopi susu menjadi menu penopang dengan kontribusi
42,7% dari total pendapatan.
Rata-rata pendapatan akhir pekan Rp 2.829.000
per hari, 57,2% di atas rata-rata hari kerja
Rp 1.799.100.
Rabu 14 Januari menjadi hari tersepi
(Rp 903.000, 45,8% dari rata-rata harian)
setelah hujan deras seharian menurut catatan Bu Rina.

2. TEMUAN UTAMA
----------------------------------------------------------------
T1. kopi susu adalah menu penopang.
    459 gelas terjual, pendapatan Rp 10.098.000
    (42,7% dari total). Stok susu dan
    biji kopi wajib diprioritaskan setiap hari.
T2. Akhir pekan adalah periode tersibuk.
    Sabtu 10 Januari Rp 2.743.000 dan
    Minggu 11 Januari Rp 2.915.000
    (tertinggi sepanjang periode). Rata-rata akhir pekan 1,57 kali
    rata-rata hari kerja.
T3. Rabu 14 Januari adalah anomali penurunan.
    Pendapatan Rp 903.000, hanya 45,8%
    dari rata-rata harian. Penyebabnya hujan deras seharian, bukan
    perubahan selera pelanggan.
T4. teh tawar menyumbang porsi terkecil.
    Kontribusinya 8,1% dari total
    pendapatan. Menu ini tetap dipertahankan sebagai pilihan non-kopi
    yang murah.

3. ANGKA KUNCI
----------------------------------------------------------------
- Pendapatan total 12 hari   : Rp 23.649.000
- Rata-rata pendapatan harian: Rp 1.970.750
- Gelas terjual              : 1.184 gelas
- Hari terbaik               : 2026-01-11 (Rp 2.915.000)
- Hari tersepi               : 2026-01-14 (Rp 903.000)
- Rata-rata akhir pekan      : Rp 2.829.000 per hari
- Rata-rata hari kerja       : Rp 1.799.100 per hari

4. REKOMENDASI
----------------------------------------------------------------
R1. Tambah porsi stok susu dan biji kopi jelang akhir pekan;
    belanja Sabtu-Minggu perlu sekitar 57,2% lebih
    banyak dari hari kerja.
R2. Siapkan strategi hari hujan: promo minuman hangat atau layanan
    pesan-antar agar hari seperti 2026-01-14 tidak
    sepenuhnya sepi.
R3. Pertahankan teh tawar sebagai menu pelengkap tanpa
    menambah stok besar; pantau kontribusinya lagi setelah tiga bulan.
R4. Perbaiki kebiasaan input kasir: data mentah memuat
    2 baris duplikat, 3 sel gelas kosong,
    1 nama menu tidak konsisten, dan
    1 pendapatan 0 dari total
    50 baris yang diekspor.

5. CATATAN KUALITAS DATA
----------------------------------------------------------------
- Baris data mentah : 50
- Baris data bersih : 48
- Pembersihan       : 2 duplikat persis dibuang,
                     3 sel gelas '-' diisi ulang dari
                     harga satuan menu, 'Kopi Susu' menjadi 'kopi susu',
                     1 pendapatan 0 dihitung ulang
                     (17 gelas teh tawar menjadi Rp 170.000).
- Seluruh angka pada laporan ini dapat direproduksi dengan
  menjalankan ulang skrip yang sama pada data mentah yang sama.
================================================================

Cara Mengerjakan #

Unduh ketiga berkas di atas ke satu folder dengan struktur yang sama (data/, src/, laporan/), lalu ikuti sepuluh bagian secara berurutan. Setiap bagian memuat kode yang bisa dijalankan langsung dan merujuk ke berkas yang sama, sehingga kondisi proyek tetap utuh di setiap tahap. Skrip analisis_penjualan.py juga muncul di bagian Berkas Sumber pada tiap halaman bagian untuk memudahkan pengunduhan.

Mari kita mulai dari fondasinya: memahami masalah yang ingin diselesaikan Bu Rina pada Bagian 1: Memahami Masalah .