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 laporanIsiSumber angka
A. Statistik per produktotal, rata-rata, std, puncak musimanst.total, st.rata2, st.devstd, st.bulanPuncak
B. Total tahunantabel 3 tahun x 3 produk + kolom totalst.totalTahunan
C. Temuan utamadelapan temuan otomatis bernomorseluruh field st
D. Figurdaftar tiga berkas PNGdaftarFigur
Catatan provenancejumlah imputasi dan koreksi datadata.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.

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:

matlab
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));
end

Mari kita bedah baris-baris kode di atas (nomor baris mengikuti berkas src/eksporLaporan.m):

  1. 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.
  2. Baris 28-29 (format header tabel): %-8s meratakan ke kiri nama produk, %10s dan seterusnya meratakan ke kanan kolom angka — lebar kolom tetap menjaga baris tetap lurus.
  3. Baris 31-34 (isi tabel): satu fprintf per baris data; %10d untuk total bulat, %12.1f satu desimal untuk rata-rata dan std, %7s (%.1f) menggabungkan nama bulan puncak dan nilainya.
  4. namaBulan{st.bulanPuncak(k)}: nomor bulan 1-12 diterjemahkan menjadi Jan-Des lewat cell array namaBulan yang 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:

matlab
% ---- 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:

  1. 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.
  2. Baris 53-59 (penomoran temuan): variabel nomor menomori temuan berurutan meski jumlah produk berubah — nomor urut tidak di-hardcode per baris.
  3. 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.
  4. Baris 69-71 (tahun terkuat): sum(st.totalTahunan, 2) menjumlahkan per baris (per tahun, lintas produk); max memilih tahun dengan total terbesar.

Laporan ditutup catatan provenance — pengakuan tentang apa yang sudah dilakukan pada data:

matlab
% ---- 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:

matlab
% 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:

text
==============================================================
 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:

matlab
% 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:

text
laporan identik antar eksekusi : 1
statistik identik              : 1

Uji 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):

matlab
    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});
matlab
    % ---- 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/:

text
== 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 gagal

Skrip 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 #

DeliverableKriteria selesai
data/penjualan.csv36 baris data + header, empat kolom
Tiga PNG di hasil/tersimpan, lebih dari 5 KB, tidak blank — diverifikasi uji
hasil/laporan_ringkasan.txtbagian A-D lengkap + catatan provenance
Temuan laporandihitung dari struct statistik, bukan diketik
Idempotensieksekusi ulang menghasilkan keluaran identik
tests/uji_pipeline.menam 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 — exportgraphics ke 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 — jsonencode struct 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.