Pada Bagian 1 kita sudah menetapkan pertanyaan analisis dan merancang dataset sintetis yang sengaja membawa tiga nilai kosong serta dua outlier. Dataset seperti itu meniru kenyataan: berkas CSV dari sistem pencatatan hampir selalu punya baris kosong dan angka tak wajar. Sekarang kita membangun dua tahap berikutnya dari pipeline — membaca berkas menjadi data numerik yang siap diolah, lalu membersihkan masalahnya dengan keputusan yang bisa dipertanggungjawabkan, bukan sekadar menghapus baris.
Yang kita kerjakan pada bagian ini:
- membaca CSV dengan guard kompatibel
readmatrix/dlmread, - validasi struktural berkas (jumlah kolom, kolom waktu, berkas ada),
- deteksi dua jenis masalah: nilai kosong (
NaN) dan kandidat outlier, - imputasi median untuk nilai kosong dan clip pagar IQR untuk outlier.
Membaca CSV dengan Guard Kompatibel #
Fungsi muatData.m di folder src/ membaca berkas CSV produksi Bagian 1. Fungsi ini memakai pola guard yang sudah kita kenal dari section Import dan Export Data: fungsi modern readmatrix (MATLAB R2019a+) dipakai bila tersedia, dan padanan klasik dlmread dengan offset header menjadi jalur cadangan.
if exist(namaFile, 'file') ~= 2
error('muatData:berkas', 'Berkas tidak ditemukan: %s', namaFile);
end
% ---- baca matriks numerik, lewati header ----
if exist('readmatrix') ~= 0
M = readmatrix(namaFile); % MATLAB R2019a+: header otomatis
else
M = dlmread(namaFile, ',', 1, 0); % padanan klasik: mulai baris ke-2
endMari kita bedah baris-baris kuncinya:
- Baris 1-3 (
if exist(namaFile, 'file') ~= 2): sebelum membaca, fungsi memastikan berkas benar-benar ada —existmengembalikan2untuk berkas biasa. Galat dengan identifiermuatData:berkasmemberi pesan yang menyebut nama berkas, jauh lebih informatif daripada galat baca mentah dari sistem. - Baris 7-8 (
if exist('readmatrix') ~= 0): guard versi. Pada MATLAB modern,readmatrixmembaca CSV dan melompati header secara otomatis; pada lingkungan tanpa fungsi tersebut (misalnya GNU Octave),dlmread(namaFile, ',', 1, 0)membaca mulai baris kedua (indeks baris1, kolom0) sehingga header teks tidak pernah masuk ke matriks numerik. - Kedua jalur menghasilkan
Myang identik — matriks 36×4: kolom pertama bulan ke-1..36, tiga kolom berikutnya penjualan kopi, teh, dan susu.
Setelah membaca, fungsi memvalidasi bentuk data sebelum lanjut:
if size(M, 2) ~= 4
error('muatData:kolom', ...
'Dataset harus punya 4 kolom, ditemukan %d.', size(M, 2));
end
if any(isnan(M(:, 1)))
error('muatData:bulan', 'Kolom bulan tidak boleh kosong.');
endValidasi ini menjaga kontrak dataset: kolom waktu tidak boleh kosong karena seluruh analisis tren bergantung padanya, sedangkan nilai kosong pada kolom penjualan masih boleh — itulah masalah yang akan kita tangani secara sadar di tahap berikutnya.
Mendeteksi Dua Jenis Masalah #
Pembacaan yang berhasil tidak berarti data sehat. Fungsi muatData kemudian memeriksa dua jenis masalah yang berbeda akar masalahnya:
% ---- nilai kosong: hitung NaN per kolom produk ----
hilangPerKolom = sum(isnan(penjualan), 1);
% ---- kandidat outlier: |z| > 2.5 pada nilai yang tersedia ----
maskOutlier = false(size(penjualan));
for k = 1:nProduk
ada = ~isnan(penjualan(:, k));
rata = mean(penjualan(ada, k));
sebar = std(penjualan(ada, k));
if sebar > 0
maskOutlier(ada, k) = ...
abs(penjualan(ada, k) - rata) / sebar > 2.5;
end
endMari kita telaah cara kerjanya:
- Baris 2 (
sum(isnan(penjualan), 1)):isnanmenghasilkan mask boolean berukuran sama dengan data; menjumlahkan sepanjang dimensi 1 menghitung banyaknya sel kosong per kolom — informasi yang jauh lebih berguna daripada satu angka total. - Baris 5-11 (loop z-score): untuk setiap kolom produk, fungsi menghitung mean dan std hanya dari nilai yang tersedia (
ada = ~isnan(...)). Selisih tiap nilai terhadap rata-rata dibagi sebaran; nilai dengan|z| > 2.5ditandai sebagai kandidat outlier. - Guard
if sebar > 0mencegah pembagian dengan nol pada kolom yang isinya seragam. - Hasilnya disimpan dalam struct
data—data.jumlahNaN,data.hilangPerKolom,data.jumlahOutlier, dandata.maskOutlier— sehingga tahap berikutnya tidak menghitung ulang.
Kita panggil kedua fungsi berurutan dan melihat ringkasan masalahnya:
path = buatDataset('kerja');
data = muatData(path);
fprintf('Baris data: %d, kolom produk: %d\n', data.jumlahBaris, size(data.penjualan,2));
fprintf('Missing (NaN) terdeteksi: %d\n', data.jumlahNaN);
fprintf('Kandidat outlier: %d\n', data.jumlahOutlier);
fprintf('NaN per kolom: %d %d %d\n', data.hilangPerKolom);Output yang Dihasilkan:
Baris data: 36, kolom produk: 3
Missing (NaN) terdeteksi: 3
Kandidat outlier: 2
NaN per kolom: 1 1 1Tiga nilai kosong terbagi rata satu per produk, tepat seperti desain dataset pada Bagian 1, dan dua nilai terlalu jauh dari kelompoknya terdeteksi sebagai kandidat outlier. Deteksi dan laporan ini persis yang diuji oleh tests/uji_pipeline.m pada kelompok uji kedua.
Membersihkan dengan Imputasi Median dan Clip IQR #
Fungsi bersihkanData.m mengambil struct dari muatData dan mengembalikan data siap analisis. Dua keputusan desainnya:
- NaN diganti median kolom, bukan mean — median tahan pencilan, sedangkan mean tertarik ke arah outlier yang kebetulan ada di kolom yang sama.
- Outlier dipotong (clip) ke pagar Tukey
[Q1 - 1.5*IQR, Q3 + 1.5*IQR], bukan dihapus — baris tetap terhitung sebagai bulan berjalan, nilainya saja dibatasi ke rentang wajar.
for k = 1:nProduk
kolom = penjualan(:, k);
% ---- 1) imputasi median pada sel NaN ----
hilang = isnan(kolom);
if any(hilang)
kolom(hilang) = median(kolom(~hilang));
jumlahDiimputasi = jumlahDiimputasi + sum(hilang);
end
% ---- 2) deteksi ulang outlier pada kolom yang sudah utuh ----
z = (kolom - mean(kolom)) / std(kolom);
pencilan = abs(z) > 2.5;Mari kita bedah logikanya:
- Baris 4-7 (imputasi): mask
hilangmenandai sel kosong;median(kolom(~hilang))menghitung median hanya dari nilai yang ada, lalu hasilnya mengisi sel kosong tersebut. - Baris 10-11 (deteksi ulang): z-score dihitung setelah imputasi. Urutan ini disengaja — mean dan std tidak terdefinisi dengan benar pada kolom yang masih berlubang, jadi deteksi outlier menunggu kolom utuh.
- Lanjutan fungsi menghitung
q1,q3, dan pagar IQR, lalu memotong nilai bertandapencilanke[pagarBawah, pagarAtas]— dengan pembulatan karena penjualan tercatat dalam unit bulat.
Verifikasi akhir tahap ini menjalankan bersihkanData dan memeriksa hasilnya:
hasil = bersihkanData(data);
fprintf('Diimputasi: %d, dikoreksi: %d\n', hasil.jumlahDiimputasi, hasil.jumlahDikoreksi);
fprintf('NaN tersisa setelah bersih: %d\n', sum(sum(isnan(hasil.penjualan))));
fprintf('Pagar IQR kolom 1: [%.1f, %.1f]\n', hasil.pagarBawah(1), hasil.pagarAtas(1));Output yang Dihasilkan:
Diimputasi: 3, dikoreksi: 2
NaN tersisa setelah bersih: 0
Pagar IQR kolom 1: [311.0, 867.0]Semua angka cocok dengan rencana: tiga sel diimputasi, dua outlier dikoreksi, dan tidak ada NaN tersisa. Pagar IQR kolom kopi ([311, 867]) juga menjelaskan mengapa dua nilai suntikan ditandai — keduanya jatuh jauh di luar pagar tersebut sebelum dipotong.
Titik Jalan Berikutnya #
Data kini utuh dan wajar: 36 baris × 3 produk tanpa NaN dan tanpa pencilan liar. Pada Bagian 3 kita mengubah data bersih tersebut menjadi tiga figur yang menjawab pertanyaan analisis — tren bulanan, distribusi per produk, dan perbandingan tahunan — semuanya digambar tanpa jendela tampilan agar pipeline tetap berjalan di server.