MIF_E31232435/ml_model/create_testing_guide_docx.py

925 lines
38 KiB
Python

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor
OUTPUT = "ml_model/Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx"
NAVY = "17365D"
BLUE = "2E74B5"
LIGHT_BLUE = "E8EEF5"
LIGHT_GRAY = "F2F4F7"
GOLD = "7A5A00"
LIGHT_GOLD = "FFF4CC"
RED = "9B1C1C"
LIGHT_RED = "FDECEC"
GREEN = "1E6B3A"
WHITE = "FFFFFF"
BLACK = "000000"
MUTED = "666666"
def set_cell_shading(cell, fill):
tc_pr = cell._tc.get_or_add_tcPr()
shd = tc_pr.find(qn("w:shd"))
if shd is None:
shd = OxmlElement("w:shd")
tc_pr.append(shd)
shd.set(qn("w:fill"), fill)
def set_cell_margins(cell, top=100, start=120, bottom=100, end=120):
tc = cell._tc
tc_pr = tc.get_or_add_tcPr()
tc_mar = tc_pr.first_child_found_in("w:tcMar")
if tc_mar is None:
tc_mar = OxmlElement("w:tcMar")
tc_pr.append(tc_mar)
for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
node = tc_mar.find(qn(f"w:{margin}"))
if node is None:
node = OxmlElement(f"w:{margin}")
tc_mar.append(node)
node.set(qn("w:w"), str(value))
node.set(qn("w:type"), "dxa")
def set_repeat_table_header(row):
tr_pr = row._tr.get_or_add_trPr()
tbl_header = OxmlElement("w:tblHeader")
tbl_header.set(qn("w:val"), "true")
tr_pr.append(tbl_header)
def set_table_borders(table, color="B7C9DC", size="6"):
tbl = table._tbl
tbl_pr = tbl.tblPr
borders = tbl_pr.first_child_found_in("w:tblBorders")
if borders is None:
borders = OxmlElement("w:tblBorders")
tbl_pr.append(borders)
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
element = borders.find(qn(f"w:{edge}"))
if element is None:
element = OxmlElement(f"w:{edge}")
borders.append(element)
element.set(qn("w:val"), "single")
element.set(qn("w:sz"), size)
element.set(qn("w:space"), "0")
element.set(qn("w:color"), color)
def set_font(run, name="Calibri", size=11, bold=False, italic=False, color=BLACK):
run.font.name = name
run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name)
run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name)
run.font.size = Pt(size)
run.bold = bold
run.italic = italic
run.font.color.rgb = RGBColor.from_string(color)
def add_page_number(paragraph):
run = paragraph.add_run()
fld_char1 = OxmlElement("w:fldChar")
fld_char1.set(qn("w:fldCharType"), "begin")
instr_text = OxmlElement("w:instrText")
instr_text.set(qn("xml:space"), "preserve")
instr_text.text = "PAGE"
fld_char2 = OxmlElement("w:fldChar")
fld_char2.set(qn("w:fldCharType"), "end")
run._r.append(fld_char1)
run._r.append(instr_text)
run._r.append(fld_char2)
set_font(run, size=9, color=MUTED)
def add_heading(doc, text, level=1):
p = doc.add_paragraph(style=f"Heading {level}")
p.add_run(text)
return p
def add_body(doc, text, bold_prefix=None):
p = doc.add_paragraph(style="Normal")
if bold_prefix and text.startswith(bold_prefix):
r1 = p.add_run(bold_prefix)
set_font(r1, bold=True)
r2 = p.add_run(text[len(bold_prefix):])
set_font(r2)
else:
r = p.add_run(text)
set_font(r)
return p
def add_bullet(doc, text, level=0):
style = "List Bullet" if level == 0 else "List Bullet 2"
p = doc.add_paragraph(style=style)
r = p.add_run(text)
set_font(r)
return p
def add_number(doc, text):
p = doc.add_paragraph(style="List Number")
r = p.add_run(text)
set_font(r)
return p
def add_formula(doc, formula, explanation=None):
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(5)
p.paragraph_format.space_after = Pt(5)
r = p.add_run(formula)
set_font(r, name="Cambria Math", size=11.5, bold=True, color=NAVY)
if explanation:
p2 = doc.add_paragraph()
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
p2.paragraph_format.space_after = Pt(6)
r2 = p2.add_run(explanation)
set_font(r2, size=9.5, italic=True, color=MUTED)
def add_callout(doc, title, body, fill=LIGHT_BLUE, title_color=NAVY):
table = doc.add_table(rows=1, cols=1)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
table.columns[0].width = Inches(6.3)
cell = table.cell(0, 0)
set_cell_shading(cell, fill)
set_cell_margins(cell, top=150, start=180, bottom=150, end=180)
p = cell.paragraphs[0]
p.paragraph_format.space_after = Pt(4)
r = p.add_run(title)
set_font(r, bold=True, color=title_color)
p2 = cell.add_paragraph()
p2.paragraph_format.space_after = Pt(0)
r2 = p2.add_run(body)
set_font(r2, size=10.5)
doc.add_paragraph().paragraph_format.space_after = Pt(1)
def add_table(doc, headers, rows, widths=None, font_size=9.5):
table = doc.add_table(rows=1, cols=len(headers))
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
set_table_borders(table)
hdr = table.rows[0]
set_repeat_table_header(hdr)
for i, header in enumerate(headers):
cell = hdr.cells[i]
set_cell_shading(cell, NAVY)
set_cell_margins(cell)
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_after = Pt(0)
r = p.add_run(str(header))
set_font(r, size=font_size, bold=True, color=WHITE)
for row_idx, row_data in enumerate(rows):
cells = table.add_row().cells
for i, value in enumerate(row_data):
cell = cells[i]
set_cell_margins(cell)
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
if row_idx % 2 == 1:
set_cell_shading(cell, LIGHT_GRAY)
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.LEFT if i == 0 else WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_after = Pt(0)
r = p.add_run(str(value))
set_font(r, size=font_size)
if widths:
for row in table.rows:
for idx, width in enumerate(widths):
row.cells[idx].width = Inches(width)
doc.add_paragraph().paragraph_format.space_after = Pt(1)
return table
def add_code(doc, text):
table = doc.add_table(rows=1, cols=1)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
cell = table.cell(0, 0)
set_cell_shading(cell, "F6F8FA")
set_cell_margins(cell, top=120, start=160, bottom=120, end=160)
p = cell.paragraphs[0]
p.paragraph_format.space_after = Pt(0)
for idx, line in enumerate(text.splitlines()):
if idx:
p.add_run("\n")
r = p.add_run(line)
set_font(r, name="Consolas", size=8.5, color="24292F")
doc.add_paragraph().paragraph_format.space_after = Pt(1)
def configure_document(doc):
section = doc.sections[0]
section.top_margin = Inches(0.85)
section.bottom_margin = Inches(0.8)
section.left_margin = Inches(1.0)
section.right_margin = Inches(1.0)
section.header_distance = Inches(0.4)
section.footer_distance = Inches(0.4)
styles = doc.styles
normal = styles["Normal"]
normal.font.name = "Calibri"
normal._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
normal._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
normal.font.size = Pt(11)
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
normal.paragraph_format.space_after = Pt(7)
normal.paragraph_format.line_spacing = 1.25
for name, size, color, before, after in (
("Heading 1", 16, BLUE, 16, 8),
("Heading 2", 13, BLUE, 12, 6),
("Heading 3", 11.5, NAVY, 9, 4),
):
style = styles[name]
style.font.name = "Calibri"
style._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
style._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
style.font.size = Pt(size)
style.font.bold = True
style.font.color.rgb = RGBColor.from_string(color)
style.paragraph_format.space_before = Pt(before)
style.paragraph_format.space_after = Pt(after)
style.paragraph_format.keep_with_next = True
for list_name in ("List Bullet", "List Bullet 2", "List Number"):
style = styles[list_name]
style.font.name = "Calibri"
style.font.size = Pt(11)
style.paragraph_format.space_after = Pt(4)
style.paragraph_format.line_spacing = 1.2
header = section.header
hp = header.paragraphs[0]
hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
r = hp.add_run("Panduan Pengujian Model Prediksi Permintaan Bahan Kue")
set_font(r, size=8.5, color=MUTED)
footer = section.footer
fp = footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = fp.add_run("Tugas Akhir | Halaman ")
set_font(r, size=9, color=MUTED)
add_page_number(fp)
def add_cover(doc):
for _ in range(5):
doc.add_paragraph()
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("PANDUAN PENGUJIAN MODEL")
set_font(r, size=25, bold=True, color=NAVY)
p.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Linear Regression dan Random Forest")
set_font(r, size=17, bold=True, color=BLUE)
p.paragraph_format.space_after = Pt(8)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Studi Kasus Prediksi Permintaan Stok Bahan Kue")
set_font(r, size=13, italic=True, color=MUTED)
p.paragraph_format.space_after = Pt(34)
add_table(
doc,
["Informasi", "Keterangan"],
[
["Jumlah data", "6.742 transaksi"],
["Pembagian data", "80% training dan 20% testing"],
["Target", "jumlah_permintaan_bahan"],
["Model yang dibandingkan", "Linear Regression dan Random Forest Regressor"],
["Tujuan dokumen", "Bahan belajar, pembahasan laporan, dan persiapan sidang"],
],
widths=[1.8, 4.5],
font_size=10,
)
for _ in range(5):
doc.add_paragraph()
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Disusun berdasarkan hasil pengujian model pada proyek tugas akhir")
set_font(r, size=10, italic=True, color=MUTED)
p.add_run().add_break(WD_BREAK.PAGE)
def build_document():
doc = Document()
configure_document(doc)
add_cover(doc)
add_heading(doc, "Ringkasan Eksekutif", 1)
add_body(
doc,
"Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest "
"dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi dan dibagi "
"menjadi 5.393 data training serta 1.349 data testing menggunakan rasio 80:20."
)
add_table(
doc,
["Model", "R² Testing", "MAE", "RMSE", "Interpretasi Awal"],
[
["Linear Regression", "0,7813", "0,63", "0,91", "Cukup baik pada test set"],
["Random Forest", "0,9964", "0,03", "0,12", "Sangat tinggi pada test set"],
],
widths=[1.6, 1.0, 0.8, 0.8, 2.1],
)
add_callout(
doc,
"Temuan Kritis",
"Nilai Random Forest yang sangat tinggi bukan sepenuhnya menunjukkan kemampuan meramalkan "
"permintaan masa depan. Kolom total_harga_update memiliki hubungan langsung dengan target: "
"total_harga_update = harga_satuan_update x jumlah_permintaan_bahan. Kondisi ini disebut data leakage.",
fill=LIGHT_RED,
title_color=RED,
)
add_body(
doc,
"Dokumen ini menjelaskan dua sudut pandang secara terpisah: pertama, bagaimana hasil testing "
"yang ada dihitung; kedua, apakah hasil tersebut valid sebagai ukuran kemampuan prediksi nyata."
)
add_heading(doc, "Daftar Isi", 1)
for item in (
"1. Gambaran Umum Pengujian Model",
"2. Persiapan dan Pembagian Data",
"3. Cara Kerja Linear Regression",
"4. Cara Kerja Random Forest",
"5. Metrik Evaluasi Model",
"6. Perhitungan Hasil Testing Proyek",
"7. Perbandingan dan Interpretasi Model",
"8. Analisis Data Leakage",
"9. Pengujian Ulang Tanpa Data Leakage",
"10. Rekomendasi Perbaikan Metodologi",
"11. Contoh Penjelasan untuk Laporan",
"12. Contoh Jawaban Saat Sidang",
"Lampiran: Ringkasan Rumus dan Istilah",
):
add_body(doc, item)
doc.add_paragraph().add_run().add_break(WD_BREAK.PAGE)
add_heading(doc, "1. Gambaran Umum Pengujian Model", 1)
add_body(
doc,
"Machine learning tidak cukup hanya dilatih. Model harus diuji menggunakan data yang tidak "
"digunakan selama proses pelatihan. Tujuannya adalah mengukur apakah pola yang dipelajari model "
"dapat diterapkan pada data lain, bukan hanya menghafal data training."
)
add_heading(doc, "1.1 Tujuan Testing", 2)
for text in (
"Menilai kedekatan hasil prediksi dengan nilai permintaan aktual.",
"Membandingkan performa Linear Regression dan Random Forest.",
"Mendeteksi overfitting, underfitting, dan kesalahan metodologi.",
"Memilih model yang paling sesuai untuk digunakan pada aplikasi.",
):
add_bullet(doc, text)
add_heading(doc, "1.2 Alur Pengujian", 2)
for text in (
"Membaca dataset transaksi.",
"Mengidentifikasi target dan fitur.",
"Mengubah data kategori menjadi angka.",
"Membagi data menjadi training set dan testing set.",
"Melatih masing-masing model menggunakan training set.",
"Menghasilkan prediksi pada testing set.",
"Menghitung R², MAE, dan RMSE.",
"Membandingkan serta mengevaluasi validitas hasil.",
):
add_number(doc, text)
add_heading(doc, "2. Persiapan dan Pembagian Data", 1)
add_heading(doc, "2.1 Struktur Dataset", 2)
add_table(
doc,
["Komponen", "Nilai"],
[
["Total data", "6.742 transaksi"],
["Training set", "5.393 transaksi atau 80%"],
["Testing set", "1.349 transaksi atau 20%"],
["Target", "jumlah_permintaan_bahan"],
["Jumlah fitur model tersimpan", "10 fitur"],
],
widths=[2.3, 4.0],
)
add_heading(doc, "2.2 Fitur Model", 2)
add_table(
doc,
["No.", "Fitur", "Makna"],
[
["1", "produk_encoded", "Kode angka produk"],
["2", "tahun", "Tahun transaksi"],
["3", "bulan", "Bulan transaksi"],
["4", "hari", "Tanggal dalam bulan"],
["5", "hari_dalam_minggu", "Kode hari dalam minggu"],
["6", "harga_satuan_update", "Harga satuan bahan"],
["7", "total_harga_update", "Total harga transaksi"],
["8", "hari_minggu", "Duplikasi informasi hari dalam minggu"],
["9", "nama_produk_encoded", "Nama produk yang telah diubah menjadi angka"],
["10", "kategori_produk_encoded", "Kategori produk yang telah diubah menjadi angka"],
],
widths=[0.6, 2.3, 3.4],
)
add_heading(doc, "2.3 Pembagian Training dan Testing", 2)
add_code(
doc,
"X_train, X_test, y_train, y_test = train_test_split(\n"
" X, y, test_size=0.2, random_state=42\n"
")"
)
add_body(
doc,
"Parameter test_size=0.2 berarti 20% data digunakan untuk testing. Parameter random_state=42 "
"membuat pembagian acak selalu sama ketika program dijalankan ulang, sehingga hasil pengujian "
"dapat direproduksi."
)
add_callout(
doc,
"Catatan untuk Data Berdasarkan Waktu",
"Karena tujuan sistem adalah memprediksi masa depan, pembagian data secara kronologis lebih kuat "
"daripada pembagian acak. Data lama sebaiknya digunakan sebagai training dan data terbaru sebagai testing.",
fill=LIGHT_GOLD,
title_color=GOLD,
)
add_heading(doc, "3. Cara Kerja Linear Regression", 1)
add_body(
doc,
"Linear Regression mencari hubungan berbentuk garis lurus antara fitur masukan dan target. "
"Setiap fitur memperoleh koefisien yang menunjukkan arah serta besarnya pengaruh terhadap hasil prediksi."
)
add_formula(doc, "ŷ = β₀ + β₁x₁ + β₂x₂ + ... + βₚxₚ", "Rumus umum prediksi Linear Regression")
add_table(
doc,
["Simbol", "Arti"],
[
["ŷ", "Nilai jumlah permintaan hasil prediksi"],
["β₀", "Intercept atau nilai awal model"],
["βᵢ", "Koefisien untuk fitur ke-i"],
["xᵢ", "Nilai fitur ke-i"],
["p", "Jumlah fitur yang digunakan"],
],
widths=[1.1, 5.2],
)
add_heading(doc, "3.1 Proses Pembelajaran", 2)
add_body(
doc,
"Koefisien dicari dengan metode Ordinary Least Squares. Model memilih koefisien yang membuat "
"jumlah kuadrat selisih antara nilai aktual dan prediksi menjadi sekecil mungkin."
)
add_formula(doc, "SSE = Σ(yᵢ - ŷᵢ)²", "SSE adalah jumlah kuadrat error prediksi")
add_formula(doc, "β = (XᵀX)⁻¹Xᵀy", "Bentuk matriks solusi Ordinary Least Squares")
add_heading(doc, "3.2 Persamaan yang Dihasilkan pada Proyek", 2)
add_formula(
doc,
"ŷ ≈ 25,704 - 0,00306(produk) - 0,01028(tahun) + 0,00289(bulan) "
"- 0,00276(hari) - 0,000208(harga satuan) + 0,0000421(total harga) + ...",
)
add_body(
doc,
"Linear Regression memperoleh R² testing sebesar 0,7813. Model ini terbantu oleh total harga, "
"tetapi tidak dapat menangkap hubungan pembagian antara total harga dan harga satuan sebaik Random Forest."
)
add_heading(doc, "3.3 Kelebihan dan Keterbatasan", 2)
add_table(
doc,
["Kelebihan", "Keterbatasan"],
[
["Mudah dijelaskan dan diinterpretasikan", "Mengasumsikan hubungan cenderung linear"],
["Proses training cepat", "Kurang baik untuk pola kompleks dan non-linear"],
["Koefisien menunjukkan arah hubungan", "Sensitif terhadap fitur yang tidak relevan dan outlier"],
],
widths=[3.15, 3.15],
)
add_heading(doc, "4. Cara Kerja Random Forest", 1)
add_body(
doc,
"Random Forest Regressor adalah metode ensemble yang membangun banyak Decision Tree. Setiap pohon "
"mempelajari sampel dan kombinasi fitur yang berbeda. Prediksi akhir diperoleh dari rata-rata hasil seluruh pohon."
)
add_code(
doc,
"RandomForestRegressor(\n"
" n_estimators=100,\n"
" random_state=42,\n"
" n_jobs=-1\n"
")"
)
add_formula(doc, "ŷRF = (ŷ₁ + ŷ₂ + ... + ŷT) / T", "Prediksi Random Forest adalah rata-rata prediksi T pohon")
add_body(doc, "Karena n_estimators=100, nilai T pada proyek ini adalah 100 pohon.")
add_heading(doc, "4.1 Proses pada Setiap Pohon", 2)
for text in (
"Mengambil sampel data training secara acak dengan pengembalian atau bootstrap.",
"Memilih sebagian fitur secara acak pada setiap kandidat percabangan.",
"Mencari batas pembagian data yang paling mengurangi error.",
"Menghasilkan prediksi dari nilai rata-rata target pada leaf node.",
"Menggabungkan prediksi seluruh pohon dengan operasi rata-rata.",
):
add_number(doc, text)
add_heading(doc, "4.2 Pemilihan Percabangan", 2)
add_body(
doc,
"Untuk regresi, Decision Tree memilih percabangan yang menurunkan Mean Squared Error. "
"Percabangan yang membuat nilai target dalam setiap kelompok semakin seragam akan dipilih."
)
add_formula(doc, "MSE = (1/n) Σ(yᵢ - ŷᵢ)²")
add_heading(doc, "4.3 Mengapa Random Forest Lebih Fleksibel", 2)
add_body(
doc,
"Berbeda dari Linear Regression, Random Forest tidak mengharuskan hubungan berbentuk garis lurus. "
"Model dapat membentuk banyak aturan kondisi sehingga mampu mempelajari interaksi kompleks antara fitur."
)
add_heading(doc, "5. Metrik Evaluasi Model", 1)
add_heading(doc, "5.1 R² atau Koefisien Determinasi", 2)
add_formula(doc, "R² = 1 - [Σ(yᵢ - ŷᵢ)² / Σ(yᵢ - ȳ)²]")
add_body(
doc,
"R² mengukur seberapa besar variasi target yang dapat dijelaskan model. Nilai mendekati 1 "
"menunjukkan prediksi semakin dekat dengan data aktual. Nilai 0 berarti performa setara dengan "
"selalu menebak rata-rata. Nilai negatif berarti model lebih buruk daripada tebakan rata-rata."
)
add_callout(
doc,
"Istilah yang Tepat",
"R² sering disebut sebagai akurasi pada laporan regresi. Istilah akademik yang lebih tepat adalah "
"koefisien determinasi atau kemampuan model menjelaskan variasi target.",
)
add_heading(doc, "5.2 Mean Absolute Error", 2)
add_formula(doc, "MAE = (1/n) Σ|yᵢ - ŷᵢ|")
add_body(
doc,
"MAE menunjukkan rata-rata besar kesalahan prediksi tanpa memperhatikan arah kesalahan. "
"Jika MAE sebesar 0,63 unit, prediksi rata-rata meleset sekitar 0,63 unit dari nilai aktual."
)
add_heading(doc, "5.3 Root Mean Squared Error", 2)
add_formula(doc, "RMSE = √[(1/n) Σ(yᵢ - ŷᵢ)²]")
add_body(
doc,
"RMSE memberikan penalti lebih besar pada kesalahan yang besar karena error dikuadratkan sebelum "
"dirata-ratakan. Semakin kecil nilai MAE dan RMSE, semakin baik kedekatan prediksi."
)
add_heading(doc, "5.4 Ringkasan Interpretasi", 2)
add_table(
doc,
["Metrik", "Arah Nilai yang Baik", "Makna"],
[
["", "Semakin mendekati 1", "Besarnya variasi target yang dijelaskan model"],
["MAE", "Semakin mendekati 0", "Rata-rata kesalahan absolut"],
["RMSE", "Semakin mendekati 0", "Kesalahan dengan penalti lebih besar untuk error besar"],
],
widths=[1.0, 1.8, 3.5],
)
add_heading(doc, "6. Perhitungan Hasil Testing Proyek", 1)
add_body(
doc,
"Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 4,9622 unit, "
"sedangkan total variasi aktual atau SST adalah 5.133,0719."
)
add_heading(doc, "6.1 Linear Regression", 2)
add_table(
doc,
["Komponen", "Nilai"],
[
["Jumlah absolute error atau SAE", "848,4592"],
["Jumlah squared error atau SSE", "1.122,3551"],
["Jumlah data testing", "1.349"],
["Total variasi aktual atau SST", "5.133,0719"],
],
widths=[3.4, 2.9],
)
add_formula(doc, "MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63")
add_formula(doc, "RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91")
add_formula(doc, "R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813")
add_body(
doc,
"Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set "
"dan rata-rata prediksinya meleset sekitar 0,63 unit."
)
add_heading(doc, "6.2 Random Forest", 2)
add_table(
doc,
["Komponen", "Nilai"],
[
["Jumlah absolute error atau SAE", "39,86"],
["Jumlah squared error atau SSE", "18,7142"],
["Jumlah data testing", "1.349"],
["Total variasi aktual atau SST", "5.133,0719"],
],
widths=[3.4, 2.9],
)
add_formula(doc, "MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03")
add_formula(doc, "RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12")
add_formula(doc, "R² = 1 - (18,7142 / 5.133,0719) = 0,9964")
add_body(
doc,
"Interpretasinya, Random Forest menjelaskan sekitar 99,64% variasi target pada testing set "
"dan rata-rata prediksinya meleset sekitar 0,03 unit. Angka ini harus dibaca bersama analisis "
"data leakage pada bagian berikutnya."
)
add_heading(doc, "6.3 Contoh Hasil Prediksi Individual", 2)
add_table(
doc,
["Aktual", "Prediksi Linear Regression", "Prediksi Random Forest", "Error Absolut RF"],
[
["4", "3,5472", "4,01", "0,01"],
["3", "2,5369", "3,00", "0,00"],
["4", "4,4879", "4,00", "0,00"],
["2", "0,7336", "2,00", "0,00"],
["7", "7,2195", "7,00", "0,00"],
],
widths=[0.9, 2.0, 2.0, 1.4],
font_size=9,
)
add_heading(doc, "7. Perbandingan dan Interpretasi Model", 1)
add_table(
doc,
["Aspek", "Linear Regression", "Random Forest"],
[
["R² testing", "0,7813", "0,9964"],
["MAE testing", "0,63 unit", "0,03 unit"],
["RMSE testing", "0,91 unit", "0,12 unit"],
["Kemampuan pola non-linear", "Terbatas", "Baik"],
["Kemudahan interpretasi", "Sangat mudah", "Lebih kompleks"],
["Risiko pada hasil proyek", "Terpengaruh leakage", "Sangat terpengaruh leakage"],
],
widths=[2.2, 2.05, 2.05],
)
add_heading(doc, "7.1 Perbandingan MAE", 2)
add_formula(doc, "Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3%")
add_body(
doc,
"Pernyataan yang tepat adalah Random Forest mengurangi MAE sekitar 95,3% dibandingkan Linear "
"Regression pada test set saat ini. Pernyataan tersebut tidak sama dengan mengatakan model "
"95,3% lebih akurat."
)
add_heading(doc, "7.2 Indikasi Overfitting", 2)
add_table(
doc,
["Model", "R² Training", "R² Testing", "Selisih"],
[
["Linear Regression", "0,7902", "0,7813", "0,0089"],
["Random Forest", "0,9995", "0,9964", "0,0031"],
],
widths=[2.0, 1.4, 1.4, 1.4],
)
add_body(
doc,
"Selisih training dan testing terlihat kecil sehingga secara angka awal model tampak stabil. "
"Namun, kestabilan ini tidak membatalkan masalah data leakage karena informasi target tersedia "
"pada kedua kelompok data melalui total harga."
)
add_heading(doc, "8. Analisis Data Leakage", 1)
add_body(
doc,
"Data leakage adalah kondisi ketika fitur model mengandung informasi yang seharusnya belum "
"tersedia pada saat prediksi dilakukan atau secara langsung dibentuk dari target. Leakage membuat "
"hasil testing terlihat sangat baik tetapi performa tersebut sulit dipertahankan pada penggunaan nyata."
)
add_formula(doc, "total_harga_update = harga_satuan_update x jumlah_permintaan_bahan")
add_formula(doc, "jumlah_permintaan_bahan = total_harga_update / harga_satuan_update")
add_callout(
doc,
"Hasil Pemeriksaan Dataset",
"Hubungan total_harga_update = harga_satuan_update x jumlah_permintaan_bahan berlaku tepat pada "
"seluruh 6.742 baris atau 100% data.",
fill=LIGHT_RED,
title_color=RED,
)
add_heading(doc, "8.1 Bukti dari Feature Importance", 2)
add_table(
doc,
["Fitur", "Feature Importance Random Forest"],
[
["total_harga_update", "62,97%"],
["harga_satuan_update", "36,18%"],
["Gabungan kedua fitur harga", "99,15%"],
["Seluruh fitur lain", "Kurang dari 1% secara individual"],
],
widths=[3.7, 2.6],
)
add_body(
doc,
"Feature importance menunjukkan bahwa Random Forest hampir sepenuhnya menggunakan total harga "
"dan harga satuan. Model terutama mempelajari cara memperoleh kembali jumlah permintaan, bukan "
"mempelajari pola permintaan berdasarkan waktu, produk, atau kategori."
)
add_heading(doc, "8.2 Mengapa Leakage Berbahaya", 2)
for text in (
"Total harga transaksi biasanya baru diketahui setelah jumlah permintaan diketahui.",
"Pada saat memprediksi permintaan masa depan, total harga belum tersedia.",
"Nilai R² yang tinggi dapat memberikan kesimpulan keliru bahwa model siap digunakan.",
"Model berpotensi gagal ketika menerima data nyata tanpa informasi target terselubung.",
):
add_bullet(doc, text)
add_heading(doc, "9. Pengujian Ulang Tanpa Data Leakage", 1)
add_body(
doc,
"Untuk mengukur kemampuan prediksi yang lebih realistis, total_harga_update dihapus dari fitur. "
"Pengujian dilakukan menggunakan random split dan chronological split."
)
add_table(
doc,
["Skenario", "Model", "", "MAE", "RMSE"],
[
["Random split tanpa total harga", "Linear Regression", "-0,0045", "1,53", "1,96"],
["Random split tanpa total harga", "Random Forest", "-0,1124", "1,64", "2,06"],
["Chronological split tanpa total harga", "Linear Regression", "-0,0025", "1,57", "1,99"],
["Chronological split tanpa total harga", "Random Forest", "-0,0831", "1,65", "2,06"],
],
widths=[2.3, 1.6, 0.8, 0.8, 0.8],
font_size=8.8,
)
add_callout(
doc,
"Makna R² Negatif",
"R² negatif berarti model lebih buruk dibandingkan metode sederhana yang selalu memprediksi "
"nilai rata-rata permintaan. Hasil ini menunjukkan fitur yang tersedia belum mengandung pola "
"permintaan yang cukup kuat setelah total harga dihapus.",
fill=LIGHT_GOLD,
title_color=GOLD,
)
add_heading(doc, "9.1 Kesimpulan Validitas", 2)
add_body(
doc,
"Angka R² sebesar 0,9964 tetap merupakan hasil perhitungan yang benar untuk konfigurasi testing "
"saat ini. Akan tetapi, angka tersebut tidak valid jika digunakan untuk menyatakan kemampuan "
"memprediksi permintaan masa depan secara murni karena terdapat data leakage."
)
add_heading(doc, "10. Rekomendasi Perbaikan Metodologi", 1)
add_heading(doc, "10.1 Perbaikan Fitur", 2)
for text in (
"Hapus total_harga_update dari fitur prediksi.",
"Hapus salah satu dari hari_dalam_minggu atau hari_minggu karena keduanya duplikat.",
"Gunakan lag permintaan, misalnya jumlah permintaan hari sebelumnya atau minggu sebelumnya.",
"Gunakan moving average permintaan dalam 7, 14, atau 30 hari terakhir.",
"Tambahkan informasi hari libur, musim, promosi, dan periode ramai.",
"Tambahkan stok sebelumnya serta histori pemakaian bahan.",
):
add_bullet(doc, text)
add_heading(doc, "10.2 Perbaikan Proses Testing", 2)
for text in (
"Gunakan chronological split: data lama untuk training dan data terbaru untuk testing.",
"Gunakan TimeSeriesSplit atau walk-forward validation untuk evaluasi yang lebih kuat.",
"Bandingkan model dengan baseline sederhana seperti rata-rata atau nilai periode sebelumnya.",
"Lakukan tuning parameter Random Forest hanya menggunakan data training.",
"Laporkan R², MAE, dan RMSE secara bersamaan.",
):
add_bullet(doc, text)
add_heading(doc, "10.3 Konsistensi Aplikasi Flutter", 2)
add_body(
doc,
"Python menggunakan kode hari 0 sampai 6, sedangkan DateTime.weekday pada Flutter menggunakan "
"1 sampai 7. Nilai harus disamakan sebelum dikirim ke model. Selain itu, pemetaan encoded produk "
"dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training."
)
add_heading(doc, "11. Contoh Penjelasan untuk Laporan", 1)
add_heading(doc, "11.1 Paragraf Metode Pengujian", 2)
add_callout(
doc,
"Contoh Penulisan",
"Pengujian model dilakukan menggunakan metode hold-out dengan membagi dataset menjadi 80% data "
"training dan 20% data testing. Dari total 6.742 data transaksi, sebanyak 5.393 data digunakan "
"untuk melatih model dan 1.349 data digunakan untuk menguji performa model. Pembagian data "
"menggunakan random_state sebesar 42 agar hasil pembagian dapat direproduksi. Evaluasi dilakukan "
"menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.",
)
add_heading(doc, "11.2 Paragraf Hasil Perbandingan", 2)
add_callout(
doc,
"Contoh Penulisan",
"Berdasarkan pengujian awal, Linear Regression menghasilkan R² sebesar 0,7813, MAE sebesar 0,63, "
"dan RMSE sebesar 0,91. Random Forest menghasilkan R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE "
"sebesar 0,12. Secara numerik, Random Forest memberikan hasil prediksi yang lebih dekat terhadap "
"nilai aktual pada testing set dibandingkan Linear Regression.",
)
add_heading(doc, "11.3 Paragraf Keterbatasan", 2)
add_callout(
doc,
"Contoh Penulisan",
"Evaluasi lanjutan menunjukkan bahwa nilai Random Forest yang sangat tinggi dipengaruhi oleh "
"penggunaan fitur total_harga_update. Fitur tersebut dihitung dari harga_satuan_update dikalikan "
"jumlah_permintaan_bahan yang menjadi target model. Kondisi ini menimbulkan data leakage karena "
"fitur mengandung informasi langsung mengenai target. Oleh karena itu, hasil R² sebesar 0,9964 "
"harus dipahami sebagai performa pada konfigurasi dataset awal dan belum sepenuhnya menggambarkan "
"kemampuan prediksi permintaan masa depan.",
fill=LIGHT_GOLD,
title_color=GOLD,
)
add_heading(doc, "11.4 Paragraf Rekomendasi Pengembangan", 2)
add_callout(
doc,
"Contoh Penulisan",
"Pengembangan berikutnya perlu menghapus fitur total harga dan menggunakan fitur yang telah tersedia "
"sebelum transaksi terjadi, seperti histori permintaan, lag permintaan, moving average, tren bulanan, "
"hari libur, promosi, dan stok sebelumnya. Pengujian juga perlu dilakukan secara kronologis agar "
"lebih sesuai dengan tujuan prediksi masa depan.",
fill="ECF7EF",
title_color=GREEN,
)
add_heading(doc, "12. Contoh Jawaban Saat Sidang", 1)
add_heading(doc, "12.1 Bagaimana Proses Testing Dilakukan?", 2)
add_body(
doc,
"Jawaban: Proses testing dilakukan dengan membagi 6.742 data menjadi 80% data training dan 20% "
"data testing. Model mempelajari pola dari 5.393 data training, kemudian menghasilkan prediksi "
"untuk 1.349 data testing yang tidak digunakan saat pelatihan. Hasil prediksi dibandingkan dengan "
"nilai aktual menggunakan R², MAE, dan RMSE."
)
add_heading(doc, "12.2 Mengapa Menggunakan Tiga Metrik?", 2)
add_body(
doc,
"Jawaban: R² digunakan untuk mengukur seberapa besar variasi target dapat dijelaskan model. MAE "
"menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan "
"penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap."
)
add_heading(doc, "12.3 Mengapa Random Forest Lebih Baik?", 2)
add_body(
doc,
"Jawaban: Pada konfigurasi testing awal, Random Forest lebih baik karena mampu menangkap hubungan "
"non-linear dan menggabungkan prediksi dari 100 Decision Tree. Namun, performa yang sangat tinggi "
"juga disebabkan total harga yang memiliki hubungan langsung dengan target."
)
add_heading(doc, "12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%?", 2)
add_body(
doc,
"Jawaban: Tidak sepenuhnya. R² sebesar 0,9964 berarti model menjelaskan sekitar 99,64% variasi "
"target pada testing set. R² bukan akurasi klasifikasi. Selain itu, nilai tersebut dipengaruhi "
"data leakage sehingga belum mewakili kemampuan prediksi masa depan secara valid."
)
add_heading(doc, "12.5 Apakah Model Mengalami Overfitting?", 2)
add_body(
doc,
"Jawaban: Selisih R² training dan testing kecil, sehingga secara angka awal tidak menunjukkan "
"overfitting yang besar. Akan tetapi, terdapat masalah yang lebih penting, yaitu data leakage. "
"Leakage membuat training dan testing sama-sama mudah diprediksi karena keduanya mengandung informasi target."
)
add_heading(doc, "12.6 Apa Perbaikan yang Harus Dilakukan?", 2)
add_body(
doc,
"Jawaban: Total harga harus dihapus dari fitur, kemudian model dilatih menggunakan fitur yang benar-benar "
"tersedia sebelum transaksi terjadi. Testing sebaiknya menggunakan pembagian berdasarkan waktu dan "
"dibandingkan dengan baseline. Fitur histori seperti lag dan moving average juga perlu ditambahkan."
)
add_heading(doc, "Lampiran: Ringkasan Rumus dan Istilah", 1)
add_table(
doc,
["Konsep", "Rumus atau Definisi Ringkas"],
[
["Linear Regression", "ŷ = β₀ + β₁x₁ + ... + βₚxₚ"],
["Random Forest", "Rata-rata prediksi dari banyak Decision Tree"],
["SSE", "Σ(yᵢ - ŷᵢ)²"],
["MAE", "(1/n) Σ|yᵢ - ŷᵢ|"],
["RMSE", "√[(1/n) Σ(yᵢ - ŷᵢ)²]"],
["", "1 - SSE/SST"],
["Overfitting", "Model sangat baik pada training tetapi buruk pada data baru"],
["Underfitting", "Model terlalu sederhana dan gagal mempelajari pola"],
["Data leakage", "Fitur mengandung informasi target atau informasi masa depan"],
["Chronological split", "Training memakai data lama, testing memakai data terbaru"],
["Baseline", "Metode sederhana sebagai pembanding minimum"],
],
widths=[2.0, 4.3],
font_size=9.3,
)
add_callout(
doc,
"Kesimpulan Akhir",
"Random Forest menghasilkan metrik terbaik pada testing awal, tetapi performa tersebut sangat "
"dipengaruhi data leakage dari total_harga_update. Untuk laporan yang bertanggung jawab secara "
"akademik, tampilkan hasil awal, jelaskan keterbatasannya, dan cantumkan rekomendasi pengujian ulang "
"tanpa fitur yang dibentuk dari target.",
fill="ECF7EF",
title_color=GREEN,
)
doc.save(OUTPUT)
print(OUTPUT)
if __name__ == "__main__":
build_document()