207 lines
7.8 KiB
Python
207 lines
7.8 KiB
Python
from pathlib import Path
|
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
import tempfile
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
SOURCE_DOC = Path("docs/Penjelasan_Rinci_KMeans_Rekomendasi_JMW_Gabungan_Update.docx")
|
|
OUT_DOC = Path("docs/Penjelasan_Rinci_KMeans_Rekomendasi_JMW_Gabungan_Update_K.docx")
|
|
|
|
WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
ET.register_namespace("w", WORD_NS)
|
|
|
|
|
|
def qn(name):
|
|
return f"{{{WORD_NS}}}{name}"
|
|
|
|
|
|
def paragraph(text="", style=None):
|
|
p = ET.Element(qn("p"))
|
|
if style:
|
|
p_pr = ET.SubElement(p, qn("pPr"))
|
|
p_style = ET.SubElement(p_pr, qn("pStyle"))
|
|
p_style.set(qn("val"), style)
|
|
|
|
run = ET.SubElement(p, qn("r"))
|
|
t = ET.SubElement(run, qn("t"))
|
|
t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
|
|
t.text = text
|
|
return p
|
|
|
|
|
|
def code_paragraph(text):
|
|
p = ET.Element(qn("p"))
|
|
p_pr = ET.SubElement(p, qn("pPr"))
|
|
p_style = ET.SubElement(p_pr, qn("pStyle"))
|
|
p_style.set(qn("val"), "Code")
|
|
|
|
run = ET.SubElement(p, qn("r"))
|
|
t = ET.SubElement(run, qn("t"))
|
|
t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
|
|
t.text = text
|
|
return p
|
|
|
|
|
|
def page_break():
|
|
p = ET.Element(qn("p"))
|
|
r = ET.SubElement(p, qn("r"))
|
|
br = ET.SubElement(r, qn("br"))
|
|
br.set(qn("type"), "page")
|
|
return p
|
|
|
|
|
|
def code_block(text):
|
|
return [code_paragraph(line) for line in text.strip("\n").splitlines()]
|
|
|
|
|
|
def new_section():
|
|
items = []
|
|
items.append(page_break())
|
|
items.append(paragraph("18. Cara Menentukan Nilai k pada K-Means", "Heading1"))
|
|
items.append(paragraph(
|
|
"Nilai k pada K-Means menunjukkan jumlah cluster atau jumlah kelompok yang akan dibentuk. Dalam sistem "
|
|
"rekomendasi mobil JMW Showroom, nilai k digunakan untuk membagi data mobil ke dalam beberapa kelompok "
|
|
"berdasarkan kemiripan harga, tahun, dan mesin_cc."
|
|
))
|
|
|
|
items.append(paragraph("18.1 Pengertian Nilai k", "Heading2"))
|
|
items.append(paragraph(
|
|
"Jika k bernilai 4, maka sistem akan membentuk empat kelompok mobil. Setiap mobil akan masuk ke salah satu "
|
|
"cluster berdasarkan jarak terdekat terhadap centroid. Semakin tepat nilai k yang digunakan, semakin baik pula "
|
|
"pembagian kelompok mobil yang dihasilkan."
|
|
))
|
|
|
|
items.append(paragraph("18.2 Nilai k pada Implementasi Sistem", "Heading2"))
|
|
items.append(paragraph(
|
|
"Pada implementasi sistem, nilai k yang digunakan adalah 4. Nilai ini dikirim oleh Laravel ke Flask API melalui "
|
|
"parameter jumlah_cluster. Dengan demikian, ketika fitur rekomendasi budget dijalankan, Flask API akan membuat "
|
|
"model K-Means dengan empat cluster."
|
|
))
|
|
items.extend(code_block("""
|
|
'filter' => [
|
|
'toleransi_harga_percent' => 10,
|
|
'jumlah_cluster' => 4,
|
|
],
|
|
"""))
|
|
items.append(paragraph(
|
|
"Parameter tersebut diterima oleh Flask API dan digunakan pada fungsi apply_kmeans_budget_filter(). Jika "
|
|
"parameter jumlah_cluster tersedia, maka nilai tersebut dipakai sebagai jumlah cluster. Jika tidak tersedia, "
|
|
"sistem menggunakan nilai default 4."
|
|
))
|
|
items.extend(code_block("""
|
|
model = build_kmeans_model(
|
|
cars,
|
|
cluster_count=to_int(filters.get("jumlah_cluster"), default=4)
|
|
)
|
|
"""))
|
|
|
|
items.append(paragraph("18.3 Pengujian Nilai k Menggunakan Silhouette Score", "Heading2"))
|
|
items.append(paragraph(
|
|
"Agar nilai k tidak dipilih secara sembarang, jumlah cluster dapat diuji menggunakan silhouette score. "
|
|
"Silhouette score digunakan untuk mengukur seberapa baik data berada di dalam cluster-nya dan seberapa jauh "
|
|
"data tersebut dari cluster lain. Nilai silhouette yang lebih tinggi menunjukkan hasil cluster yang lebih baik."
|
|
))
|
|
items.extend(code_block("""
|
|
for k in range(2, 7):
|
|
kmeans = KMeans(
|
|
n_clusters=k,
|
|
random_state=42,
|
|
n_init=10
|
|
)
|
|
|
|
labels = kmeans.fit_predict(fitur_scaled)
|
|
score = silhouette_score(fitur_scaled, labels)
|
|
|
|
print(f"Jumlah Cluster: {k}, Silhouette Score: {score}")
|
|
"""))
|
|
items.append(paragraph(
|
|
"Kode tersebut mencoba beberapa nilai k, yaitu 2 sampai 6. Setiap nilai k diuji dengan K-Means, kemudian "
|
|
"hasil cluster-nya dievaluasi menggunakan silhouette score. Dari hasil tersebut, peneliti dapat melihat nilai "
|
|
"k mana yang memberikan kualitas cluster terbaik."
|
|
))
|
|
|
|
items.append(paragraph("18.4 Contoh Hasil Pengujian", "Heading2"))
|
|
items.append(paragraph(
|
|
"Misalnya hasil pengujian silhouette score menghasilkan nilai sebagai berikut:"
|
|
))
|
|
items.extend(code_block("""
|
|
Jumlah Cluster: 2, Silhouette Score: 0,2281
|
|
Jumlah Cluster: 3, Silhouette Score: 0,2345
|
|
Jumlah Cluster: 4, Silhouette Score: 0,2294
|
|
Jumlah Cluster: 5, Silhouette Score: 0,2394
|
|
Jumlah Cluster: 6, Silhouette Score: 0,2570
|
|
"""))
|
|
items.append(paragraph(
|
|
"Dari contoh tersebut, k = 6 memiliki nilai silhouette score tertinggi. Namun pada implementasi sistem, k = 4 "
|
|
"digunakan karena sistem ingin membagi mobil ke dalam empat kelompok utama yang lebih mudah diterapkan pada "
|
|
"fitur rekomendasi budget, misalnya kelompok mobil ekonomis, menengah, tinggi, dan premium. Pemilihan k = 4 "
|
|
"juga disesuaikan dengan konfigurasi yang sudah digunakan pada Flask API."
|
|
))
|
|
|
|
items.append(paragraph("18.5 Alasan Sistem Menggunakan k = 4", "Heading2"))
|
|
items.append(paragraph(
|
|
"Nilai k = 4 digunakan karena pembagian empat cluster dianggap cukup untuk merepresentasikan kelompok mobil "
|
|
"berdasarkan harga, tahun, dan mesin_cc. Dengan empat cluster, sistem dapat memisahkan mobil ke dalam beberapa "
|
|
"kelompok karakteristik tanpa membuat pembagian terlalu banyak dan sulit diinterpretasikan."
|
|
))
|
|
items.append(paragraph(
|
|
"Secara sederhana, empat cluster dapat diinterpretasikan sebagai kelompok mobil dengan karakteristik berikut: "
|
|
"cluster harga rendah atau ekonomis, cluster harga menengah, cluster harga tinggi, dan cluster premium atau "
|
|
"spesifikasi lebih tinggi. Nama atau karakteristik akhir setiap cluster tetap perlu dilihat dari rata-rata "
|
|
"data pada masing-masing cluster."
|
|
))
|
|
|
|
items.append(paragraph("18.6 Kalimat untuk Laporan", "Heading2"))
|
|
items.append(paragraph(
|
|
"Nilai k pada metode K-Means ditentukan dengan mempertimbangkan hasil pengujian jumlah cluster dan kebutuhan "
|
|
"implementasi sistem. Pada sistem ini, jumlah cluster yang digunakan adalah k = 4. Nilai tersebut digunakan "
|
|
"untuk membagi data mobil menjadi empat kelompok berdasarkan atribut harga, tahun, dan mesin_cc. Pemilihan "
|
|
"k = 4 juga disesuaikan dengan konfigurasi sistem rekomendasi pada Flask API melalui parameter "
|
|
"jumlah_cluster = 4."
|
|
))
|
|
|
|
return items
|
|
|
|
|
|
def append_section():
|
|
OUT_DOC.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp_dir = Path(tmp)
|
|
|
|
with ZipFile(SOURCE_DOC, "r") as archive:
|
|
archive.extractall(tmp_dir)
|
|
|
|
document_path = tmp_dir / "word" / "document.xml"
|
|
root = ET.parse(document_path).getroot()
|
|
body = root.find(qn("body"))
|
|
|
|
sect_pr = None
|
|
for child in list(body):
|
|
if child.tag == qn("sectPr"):
|
|
sect_pr = child
|
|
body.remove(child)
|
|
break
|
|
|
|
for item in new_section():
|
|
body.append(item)
|
|
|
|
if sect_pr is not None:
|
|
body.append(sect_pr)
|
|
|
|
ET.ElementTree(root).write(document_path, encoding="utf-8", xml_declaration=True)
|
|
|
|
if OUT_DOC.exists():
|
|
OUT_DOC.unlink()
|
|
|
|
with ZipFile(OUT_DOC, "w", ZIP_DEFLATED) as archive:
|
|
for path in tmp_dir.rglob("*"):
|
|
if path.is_file():
|
|
archive.write(path, path.relative_to(tmp_dir).as_posix())
|
|
|
|
print(OUT_DOC.resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
append_section()
|