Baris valid dari Bagian 2 sudah rapi dalam vector, tetapi daftar enam transaksi belum menjawab pertanyaan pemilik toko: produk mana yang paling menyumbang pendapatan, berapa rata-rata nilai transaksi Buku Tulis, dan dalam rentang harga berapa Pensil 2B dijual. Jawaban semacam ini lahir dari agregasi — mengelompokkan baris menurut kunci, dalam hal ini nama produk, lalu merangkum setiap kelompok menjadi beberapa angka. Bagian 3 membangun tahap itu dengan pola yang sangat umum di pemrosesan data: satu std::map dari kunci ke struct akumulator, diisi sekali jalan lewat loop sederhana. Pola ini akan Anda temui lagi di banyak tempat, dari penghitungan frekuensi kata sampai pengolahan log, jadi luangkan waktu untuk memahaminya betul-betul.
Struct Akumulator: StatistikProduk #
Sebelum mengelompokkan, kita tetapkan dulu angka apa saja yang ingin dikumpulkan per produk. Struct StatistikProduk menampung lima nilai:
| Field | Isi | Bertumbuh lewat |
|---|---|---|
transaksi | banyak baris untuk produk ini | ++ setiap baris |
unit | total barang terjual | penjumlahan jumlah |
pendapatan | total nilai transaksi (jumlah * harga) | penjumlahan nilai() |
hargaMin | harga terendah yang tercatat | pembanding < |
hargaMaks | harga tertinggi yang tercatat | pembanding > |
Perhatikan tidak ada field rataRata. Rata-rata adalah nilai turunan — pendapatan dibagi transaksi — dan menurunkannya saat pencetakan lebih baik daripada menyimpannya. Bila kita menyimpan dan terus memperbarui rata-rata di setiap baris, angkanya dihitung bertingkat dan menumpuk pembulatan dari pembagian berulang; bila kita hanya menyimpan dua total, rata-rata dihitung satu kali dari nilai yang sudah final. Keduanya sepele di dataset enam baris, tetapi perbedaannya terasa pada berkas berisi ratusan ribu transaksi.
agregasi: Satu Map, Satu Loop #
Function agregasi kita hanya belasan baris, tetapi setiap barisnya memuat keputusan:
std::map<std::string, StatistikProduk> agregasi(
const std::vector<BarisPenjualan>& baris) {
std::map<std::string, StatistikProduk> hasil;
for (const BarisPenjualan& b : baris) {
StatistikProduk& s = hasil[b.produk]; // cari atau buat akumulator
if (s.transaksi == 0 || b.harga < s.hargaMin) {
s.hargaMin = b.harga; // baris pertama: nilai awal min
}
if (s.transaksi == 0 || b.harga > s.hargaMaks) {
s.hargaMaks = b.harga; // baris pertama: nilai awal maks
}
++s.transaksi;
s.unit += b.jumlah;
s.pendapatan += b.nilai();
}
return hasil;
}Mari kita bedah:
StatistikProduk& s = hasil[b.produk];—operator[]milikstd::mapmencari kuncib.produk; bila belum ada, ia membuat entri baru berisi struct yang semua fieldnya nol, lalu mengembalikan reference ke entri itu. Satu ekspresi untuk “cari atau buat” — pola khas agregasi. Reference penting: tanpa&,sakan menjadi salinan dan seluruh penjumlahan menguap ke udara.if (s.transaksi == 0 || b.harga < s.hargaMin)— untuk baris pertama sebuah produk,hargaMinmasih 0 sehingga tidak bisa dipakai pembanding. Sentinels.transaksi == 0menandai “ini baris pertama produk ini” dan memaksahargaMinmaupunhargaMaksdiinisialisasi dari data sungguhan — jauh lebih bersih daripada mengisi awal dengan nilai ekstrem sepertiLLONG_MAX.++s.transaksi; s.unit += b.jumlah; s.pendapatan += b.nilai();— tiga akumulasi biasa.b.nilai()adalahjumlah * hargadari structBarisPenjualan, dihitung dalamlong longsehingga aman untuk angka penjualan yang besar.return hasil;— map dikembalikan utuh; urutan isinya sudah terurut menurut kunci, sesuatu yang kita manfaatkan di bawah.
std::map vs unordered_map: Kunci Determinisme #
Pertanyaan yang wajar: kenapa std::map, bukan std::unordered_map yang biasanya lebih cepat? Keduanya sama-sama bisa mengagregasi, tetapi bedanya terletak pada urutan iterasi. std::map menyimpan entri terurut menurut kunci — untuk string, urutan kamus (leksikografis) — sehingga setiap iterasi for pasti menghasilkan urutan yang sama: Buku Tulis, Penghapus, Pensil 2B, Pulpen Gel. Sementara urutan iterasi std::unordered_map tidak ditentukan standar; ia bisa berbeda antar compiler, antar platform, bahkan antar eksekusi. Karena laporan kita mencetak satu baris per produk, pilihan map membuat keluaran program deterministik dan sekalian rapi — urutan alfabetis — tanpa perlu pengurutan terpisah.
Rata-rata dan Jebakan Pembagian Bulat #
Ada satu jebakan aritmetika yang menunggu di tahap ini. Kedua operan pembagian rata-rata bertipe bilangan bulat — pendapatan adalah long long, transaksi adalah int — dan pembagian dua bilangan bulat di C++ membuang pecahannya: 263750 / 6 menghasilkan 43958, bukan 43958.33. Solusinya satu kata: cast sebelum membagi.
const double rataRata = static_cast<double>(s.pendapatan) / s.transaksi;Posisi cast-nya menentukan segalanya. static_cast<double>(s.pendapatan) mengubah operan kiri menjadi double sebelum pembagian, sehingga pembagian berlangsung sebagai pembagian riil dan pecahan terselamatkan. Menulis (double)(s.pendapatan / s.transaksi) justru terlambat — pecahan sudah hilang di dalam kurung sebelum cast menyentuhnya. Pembagian riil untuk dataset kita menghasilkan angka seperti 43958.3333...; formatnya menjadi dua desimal baru dilakukan saat pencetakan di Bagian 4 dengan std::setprecision.
Demo: Statistik per Produk #
Program demo berikut mengambil enam baris valid dari dataset (karena validasi sudah selesai di Bagian 2, barisnya langsung kita definisikan di kode), mengagregasikannya, lalu mencetak tabel ringkas beserta baris TOTAL:
// statistik-demo.cpp — demo agregasi statistik per produk dari Bagian 3.
//
// Baris valid hasil Bagian 2 langsung didefinisikan di dalam kode agar
// demo fokus pada agregasi, bukan pembacaan ulang berkas. std::map
// dipilih agar hasil cetak selalu terurut menurut nama produk —
// output jadi deterministik tanpa pengurutan tambahan.
//
// g++ -std=c++23 -Wall -Wextra statistik-demo.cpp -o statistik-demo
// ./statistik-demo
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
#include <vector>
// Satu baris penjualan yang sudah lolos validasi (dari Bagian 2).
struct BarisPenjualan {
std::string tanggal;
std::string produk;
long long jumlah = 0;
long long harga = 0;
long long nilai() const { return jumlah * harga; } // nilai transaksi
};
// Akumulator per produk.
struct StatistikProduk {
int transaksi = 0; // banyak baris untuk produk ini
long long unit = 0; // total barang terjual
long long pendapatan = 0; // total nilai transaksi
long long hargaMin = 0; // harga terendah yang tercatat
long long hargaMaks = 0; // harga tertinggi yang tercatat
};
// Mengagregasi baris valid per produk ke dalam std::map.
std::map<std::string, StatistikProduk> agregasi(
const std::vector<BarisPenjualan>& baris) {
std::map<std::string, StatistikProduk> hasil;
for (const BarisPenjualan& b : baris) {
StatistikProduk& s = hasil[b.produk]; // cari atau buat akumulator
if (s.transaksi == 0 || b.harga < s.hargaMin) {
s.hargaMin = b.harga; // baris pertama: nilai awal min
}
if (s.transaksi == 0 || b.harga > s.hargaMaks) {
s.hargaMaks = b.harga; // baris pertama: nilai awal maks
}
++s.transaksi;
s.unit += b.jumlah;
s.pendapatan += b.nilai();
}
return hasil;
}
int main() {
// Enam baris valid dari dataset data/penjualan.csv (Bagian 2).
const std::vector<BarisPenjualan> penjualan{
{"2025-11-01", "Buku Tulis", 10, 3500},
{"2025-11-01", "Pensil 2B", 25, 2000},
{"2025-11-02", "Pulpen Gel", 15, 4500},
{"2025-11-03", "Buku Tulis", 7, 3750},
{"2025-11-03", "Penghapus", 30, 1500},
{"2025-11-04", "Pensil 2B", 20, 2000},
};
const std::map<std::string, StatistikProduk> statistik = agregasi(penjualan);
std::cout << std::left << std::setw(12) << "produk" << std::right
<< std::setw(10) << "transaksi" << std::setw(7) << "unit"
<< std::setw(12) << "pendapatan" << std::setw(11) << "rata-rata"
<< std::setw(7) << "min" << std::setw(7) << "maks" << '\n';
std::cout << std::string(66, '-') << '\n';
int totalTransaksi = 0;
long long totalUnit = 0;
long long totalPendapatan = 0;
for (const auto& [produk, s] : statistik) {
// Rata-rata harus dihitung dalam double: pembagian dua long long
// akan membuang pecahan.
const double rataRata =
static_cast<double>(s.pendapatan) / s.transaksi;
std::cout << std::left << std::setw(12) << produk << std::right
<< std::setw(10) << s.transaksi << std::setw(7) << s.unit
<< std::setw(12) << s.pendapatan << std::setw(11)
<< std::fixed << std::setprecision(2) << rataRata
<< std::setw(7) << s.hargaMin << std::setw(7) << s.hargaMaks
<< '\n';
totalTransaksi += s.transaksi;
totalUnit += s.unit;
totalPendapatan += s.pendapatan;
}
const double rataTotal =
static_cast<double>(totalPendapatan) / totalTransaksi;
std::cout << std::string(66, '-') << '\n'
<< std::left << std::setw(12) << "TOTAL" << std::right
<< std::setw(10) << totalTransaksi << std::setw(7) << totalUnit
<< std::setw(12) << totalPendapatan << std::setw(11)
<< std::fixed << std::setprecision(2) << rataTotal << '\n';
}g++ -std=c++23 -Wall -Wextra statistik-demo.cpp -o statistik-demo
./statistik-demoOutput yang Dihasilkan:
produk transaksi unit pendapatan rata-rata min maks
------------------------------------------------------------------
Buku Tulis 2 17 61250 30625.00 3500 3750
Penghapus 1 30 45000 45000.00 1500 1500
Pensil 2B 2 45 90000 45000.00 2000 2000
Pulpen Gel 1 15 67500 67500.00 4500 4500
------------------------------------------------------------------
TOTAL 6 107 263750 43958.33Cermati tiga hal dari keluaran itu. Urutan barisnya leksikografis — Buku Tulis, Penghapus, Pensil 2B, Pulpen Gel — persis janji std::map di atas, tanpa satu baris sort pun. Angka-angkanya bisa diverifikasi dengan tangan: Buku Tulis mencatat 10 x 3500 + 7 x 3750 = 61250 dengan rata-rata 30625.00, dan baris TOTAL menunjukkan 43958.33 — bukti pembagian riil bekerja, karena pembagian bulat akan menghasilkan 43958. Dan rentang harga Buku Tulis (3500–3750) memperlihatkan sentinel min/maks bekerja pada produk yang muncul lebih dari sekali.
Semua angka laporan sudah siap dan teruji. Yang tersisa hanyalah pembungkusnya: tabel berformat rapi dengan kolom sejajar, daftar baris yang ditolak, dan pengujian yang mengunci semuanya — tugas Bagian 4, bagian penutup proyek ini.
Rujukan resmi: std::map (cppreference) , std::setprecision (cppreference) , static_cast (cppreference) .