Tiga figur dari Bagian 3 menghibur mata, tetapi keputusan akhirnya diambil dari angka yang bisa dikutip. Bagian penutup proyek ini merangkai statistik dan figur menjadi laporan_ringkasan.txt — laporan teks ringkas yang bisa dibuka tanpa MATLAB — lalu membuktikan bahwa seluruh pipeline layak dipercaya lewat dua sifat: idempoten dan teruji end-to-end.
Yang kita bangun pada bagian ini:
- struktur laporan empat bagian plus catatan provenance,
- bedah
eksporLaporan.m— tabel terformat dan temuan yang dihitung, bukan diketik, - isi laporan nyata dari satu eksekusi pipeline,
- idempotensi dan uji akhir
tests/uji_pipeline.m, - checklist deliverable serta ide pengembangan.
Struktur Laporan Teks #
Teks dipilih sebagai format akhir karena bisa dibuka di perangkat apa pun, mudah dikirim, dan — karena deterministik — perubahannya terlacak rapi di version control: diff yang muncul adalah perubahan data yang sesungguhnya, bukan artifact format. eksporLaporan(data, st, daftarFigur, folderHasil) menulis laporan dari empat sumber yang sudah ada di tangan:
| Bagian laporan | Isi | Sumber angka |
|---|---|---|
| A. Statistik per produk | total, rata-rata, std, puncak musiman | st.total, st.rata2, st.devstd, st.bulanPuncak |
| B. Total tahunan | tabel 3 tahun x 3 produk + kolom total | st.totalTahunan |
| C. Temuan utama | delapan temuan otomatis bernomor | seluruh field st |
| D. Figur | daftar tiga berkas PNG | daftarFigur |
| Catatan provenance | jumlah imputasi dan koreksi data | data.jumlahDiimputasi, data.jumlahDikoreksi |
Tidak ada satu angka pun yang diketik manual — semuanya mengalir dari struct yang dihitung tahap-tahap sebelumnya. Konsekuensinya: laporan tidak pernah bisa bertentangan dengan tabel Command Window, karena keduanya lahir dari sumber yang sama.
Menulis Tabel Terformat #
Fungsi membuka berkas dengan fopen(pathLengkap, 'w') — mode tulis yang selalu memulai dari baris pertama, menimpa isi lama — lalu menjaga keluaran dengan guard error bila berkas gagal dibuat. Bagian pembuka dan tabel pertamanya:
fprintf(fid, '==============================================================\n');
fprintf(fid, ' LAPORAN RINGKASAN PENJUALAN - %d BULAN (JAN %d - DES %d)\n', ...
st.jumlahBulan, st.tahun(1), st.tahun(end));
fprintf(fid, '==============================================================\n\n');
% ---- A. statistik per produk ----
fprintf(fid, 'A. STATISTIK PER PRODUK (1 bulan = 1 observasi)\n');
fprintf(fid, '--------------------------------------------------------------\n');
fprintf(fid, '%-8s %10s %12s %10s %14s\n', ...
'Produk', 'Total', 'Rata-rata', 'Std', 'Puncak musiman');
for k = 1:numel(st.namaProduk)
fprintf(fid, '%-8s %10d %12.1f %10.1f %7s (%.1f)\n', ...
st.namaProduk{k}, st.total(k), st.rata2(k), st.devstd(k), ...
namaBulan{st.bulanPuncak(k)}, st.nilaiPuncak(k));
endMari kita bedah baris-baris kode di atas (nomor baris mengikuti berkas src/eksporLaporan.m):
- Baris 21-23 (judul): judul pun diisi dari struct — jumlah bulan dan rentang tahun diambil dari
st.jumlahBulan,st.tahun(1),st.tahun(end). Ganti periode dataset, judul ikut menyesuaikan tanpa disentuh. - Baris 28-29 (format header tabel):
%-8smeratakan ke kiri nama produk,%10sdan seterusnya meratakan ke kanan kolom angka — lebar kolom tetap menjaga baris tetap lurus. - Baris 31-34 (isi tabel): satu
fprintfper baris data;%10duntuk total bulat,%12.1fsatu desimal untuk rata-rata dan std,%7s (%.1f)menggabungkan nama bulan puncak dan nilainya. namaBulan{st.bulanPuncak(k)}: nomor bulan 1-12 diterjemahkan menjadiJan-Deslewat cell arraynamaBulanyang didefinisikan di awal fungsi — angka tetap angka di struct, nama hanya lapisan tampilan.
Bagian B (total tahunan) memakai pola fprintf yang sama persis, sehingga tidak perlu dibedah ulang.
Temuan yang Dihitung, Bukan Diketik #
Bagian C adalah jantung laporan: delapan temuan yang dirumuskan program, bukan penulisnya. Selisihnya menentukan kredibilitas — manusia bisa salah kutip, kode tidak:
% ---- C. temuan utama (otomatis dari statistik) ----
fprintf(fid, '\nC. TEMUAN UTAMA\n');
fprintf(fid, '--------------------------------------------------------------\n');
[~, terbaik] = max(st.total);
fprintf(fid, '1. Produk dengan total tertinggi: %s (%d unit).\n', ...
st.namaProduk{terbaik}, st.total(terbaik));
nomor = 2;
for k = 1:numel(st.namaProduk)
fprintf(fid, '%d. Puncak musiman %s: %s (rata-rata %.1f unit/bulan).\n', ...
nomor, st.namaProduk{k}, namaBulan{st.bulanPuncak(k)}, ...
st.nilaiPuncak(k));
nomor = nomor + 1;
end
for k = 1:numel(st.namaProduk)
naik1 = 100 * (st.totalTahunan(2, k) - st.totalTahunan(1, k)) ...
/ st.totalTahunan(1, k);
naik2 = 100 * (st.totalTahunan(3, k) - st.totalTahunan(2, k)) ...
/ st.totalTahunan(2, k);
fprintf(fid, '%d. Tren tahunan %s: %+.1f%% lalu %+.1f%%.\n', ...
nomor, st.namaProduk{k}, naik1, naik2);
nomor = nomor + 1;
end
[~, tahunKuat] = max(sum(st.totalTahunan, 2));
fprintf(fid, '%d. Tahun terkuat: %d (total %d unit).\n', nomor, ...
st.tahun(tahunKuat), sum(st.totalTahunan(tahunKuat, :)));Mari kita bedah baris-baris kode di atas:
- Baris 50-52 (
[~, terbaik] = max(st.total);…): temuan pertama dipilih oleh data — indeks nilai maksimum, lalu nama dan angkanya dibaca dari struct. Klaim “produk dengan total tertinggi” tidak mungkin salah ketik. - Baris 53-59 (penomoran temuan): variabel
nomormenomori temuan berurutan meski jumlah produk berubah — nomor urut tidak di-hardcode per baris. - Baris 60-68 (tren tahunan): kenaikan persen dihitung langsung dari tabel total tahunan —
(tahun ini - tahun lalu) / tahun lalu * 100; format%+.1f%%selalu mencetak tanda, jadi naik dan turun terbaca sekali lihat. - Baris 69-71 (tahun terkuat):
sum(st.totalTahunan, 2)menjumlahkan per baris (per tahun, lintas produk);maxmemilih tahun dengan total terbesar.
Laporan ditutup catatan provenance — pengakuan tentang apa yang sudah dilakukan pada data:
% ---- catatan provenance ----
fprintf(fid, '\nCatatan: %d nilai kosong diimputasi median, %d outlier dikoreksi clip IQR.\n', ...
data.jumlahDiimputasi, data.jumlahDikoreksi);
fprintf(fid, 'Laporan dihasilkan otomatis oleh jalankanPipeline.m.\n');Angka 3 dan 2 pada baris terakhir laporan bukan teks tetap — keduanya field hasil pembersihan Bagian 2, sehingga pembaca tahu persis berapa sel data yang dimodifikasi sebelum dianalisis.
Isi Laporan Nyata #
Driver berikut menjalankan pipeline penuh (keluaran tahapannya diredakan evalc karena transkripnya sudah kita lihat di halaman ringkasan proyek), lalu membaca isi laporannya:
% driver: jalankan pipeline penuh (keluaran tahapan reda), lalu baca laporan
addpath('src');
evalc('hasil = jalankanPipeline(''latihan'');');
type(fullfile('latihan', 'hasil', 'laporan_ringkasan.txt'));Output yang Dihasilkan:
==============================================================
LAPORAN RINGKASAN PENJUALAN - 36 BULAN (JAN 2022 - DES 2024)
==============================================================
A. STATISTIK PER PRODUK (1 bulan = 1 observasi)
--------------------------------------------------------------
Produk Total Rata-rata Std Puncak musiman
kopi 21403 594.5 105.8 Des (718.7)
teh 12358 343.3 59.8 Jun (411.0)
susu 19580 543.9 74.3 Des (607.3)
B. TOTAL TAHUNAN
--------------------------------------------------------------
Tahun kopi teh susu Total
2022 6713 3805 5573 16091
2023 6961 4200 6553 17714
2024 7729 4353 7454 19536
C. TEMUAN UTAMA
--------------------------------------------------------------
1. Produk dengan total tertinggi: kopi (21403 unit).
2. Puncak musiman kopi: Des (rata-rata 718.7 unit/bulan).
3. Puncak musiman teh: Jun (rata-rata 411.0 unit/bulan).
4. Puncak musiman susu: Des (rata-rata 607.3 unit/bulan).
5. Tren tahunan kopi: +3.7% lalu +11.0%.
6. Tren tahunan teh: +10.4% lalu +3.6%.
7. Tren tahunan susu: +17.6% lalu +13.7%.
8. Tahun terkuat: 2024 (total 19536 unit).
D. FIGUR YANG DIHASILKAN
--------------------------------------------------------------
1. hasil/tren_bulanan.png
2. hasil/distribusi_penjualan.png
3. hasil/perbandingan_tahunan.png
Catatan: 3 nilai kosong diimputasi median, 2 outlier dikoreksi clip IQR.
Laporan dihasilkan otomatis oleh jalankanPipeline.m.Idempotensi: Eksekusi Ulang Tanpa Kejutan #
Pipeline dijanjikan idempoten: menjalankan ulang pada folder yang sama menghasilkan keluaran identik. Janji itu lahir dari dua keputusan desain — seluruh data dibangun dari formula deterministik tanpa RNG, dan fopen mode 'w' selalu menulis dari awal alih-alih menempel di eksekusi sebelumnya. Mari kita buktikan, bukan hanya mempercayainya:
% driver: uji cepat idempotensi laporan
addpath('src');
evalc('hasil1 = jalankanPipeline(''latihan'');');
teks1 = fileread(fullfile('latihan', 'hasil', 'laporan_ringkasan.txt'));
evalc('hasil2 = jalankanPipeline(''latihan'');');
teks2 = fileread(fullfile('latihan', 'hasil', 'laporan_ringkasan.txt'));
fprintf('laporan identik antar eksekusi : %d\n', strcmp(teks1, teks2));
fprintf('statistik identik : %d\n', ...
isequal(hasil1.statistik, hasil2.statistik));Output yang Dihasilkan:
laporan identik antar eksekusi : 1
statistik identik : 1Uji Akhir End-to-End #
Berkas tests/uji_pipeline.m memeriksa seluruh pipeline dalam enam kelompok: keutuhan CSV, deteksi masalah oleh muatData, efek pembersihan, kecocokan statistik dengan hitungan manual, keberadaan tiga figur dan laporan, serta idempotensi. Dua cuplikan kuncinya (keduanya berada di dalam blok try, indentasi aslinya dipertahankan):
for k = 1:numel(hasil.daftarFigur)
pathFigur = fullfile(folderUji, hasil.daftarFigur{k});
assert(exist(pathFigur, 'file') == 2, ...
'figur tidak ditemukan: %s', hasil.daftarFigur{k});
infoFigur = dir(pathFigur);
assert(infoFigur.bytes > 5000, ...
'figur terlalu kecil (mencurigakan kosong): %s', ...
hasil.daftarFigur{k}); % ---- 6: idempoten — eksekusi ulang memberi hasil identik ----
laporanRun1 = fileread(pathLaporan);
evalc('hasil2 = jalankanPipeline(folderUji);');
assert(isequal(hasil.statistik, hasil2.statistik), ...
'statistik berubah antar eksekusi');
assert(isequal(hasil.dataBersih.penjualan, ...
hasil2.dataBersih.penjualan), 'data bersih berubah antar eksekusi');
assert(strcmp(laporanRun1, fileread(fullfile(folderUji, ...
hasil2.pathLaporan))), 'isi laporan berubah antar eksekusi');Cuplikan pertama menolak figur yang hilang maupun figur “berhasil” tapi kosong — berkas di bawah 5 KB dianggap mencurigakan, dan uji melanjutkan dengan membaca piksel PNG untuk memastikan jumlah warna unik lebih dari lima. Cuplikan kedua adalah janji idempotensi di atas, ditulis sebagai assert yang menggagalkan uji bila dilanggar. Menjalankan berkas uji dari folder tests/:
== uji_pipeline.m ==
LULUS: CSV terbentuk dengan 36 baris data
LULUS: muatData menemukan 3 NaN dan 2 kandidat outlier
LULUS: pembersihan mengimputasi 3 NaN dan meng-clip 2 outlier
LULUS: statistik identik dengan hitungan manual
LULUS: 3 figur PNG dan laporan lengkap terbentuk
LULUS: eksekusi ulang idempoten (hasil identik)
folder uji dibersihkan: tidak ada residu di /tmp
Ringkasan: 6 kelompok uji lulus, 0 gagalSkrip uji bekerja pada folder tempname di folder temp sistem dan menghapusnya di akhir — sukses maupun gagal — sehingga pengujian tidak pernah meninggalkan residu di folder proyek.
Checklist Deliverable #
| Deliverable | Kriteria selesai |
|---|---|
data/penjualan.csv | 36 baris data + header, empat kolom |
Tiga PNG di hasil/ | tersimpan, lebih dari 5 KB, tidak blank — diverifikasi uji |
hasil/laporan_ringkasan.txt | bagian A-D lengkap + catatan provenance |
| Temuan laporan | dihitung dari struct statistik, bukan diketik |
| Idempotensi | eksekusi ulang menghasilkan keluaran identik |
tests/uji_pipeline.m | enam kelompok uji lulus, folder uji bersih |
Ide Pengembangan #
- laporan multi-format — teks yang sama bisa dirender ke HTML atau Markdown untuk wiki tim, atau PDF untuk arsip resmi,
- figur vektor —
exportgraphicske PDF/SVG (R2020b+; MATLAB-only) supaya tajam dicetak di ukuran apa pun, - temuan tambahan otomatis — produk paling fluktuatif (koefisien variasi
std/mean), korelasi penjualan antar produk, bulan dengan penyimpangan terbesar, - parameter input — ambang z pencilan, jumlah bin histogram, dan dpi sebagai argumen opsional, sehingga pipeline bisa dikalibrasi tanpa menyentuh kode,
- ringkasan mesin —
jsonencodestruct hasil (MATLAB R2018a+; MATLAB-only) untuk dikonsumsi pipeline lain di hilir.
Proyek tuntas: pertanyaan analisis (Bagian 1) berubah jadi data, data berubah jadi gambar (Bagian 2-3), dan gambar berubah jadi laporan yang bisa dikirim. Pola yang kita bangun — satu fungsi satu tanggung jawab, orkestrator tipis, uji otomatis — tinggal dipindahkan ke dataset nyata: ganti buatDataset dengan pembacaan CSV sungguhan, dan lima modul lainnya berjalan tanpa diubah.