599 lines
26 KiB
Python
599 lines
26 KiB
Python
from html import escape
|
|
from pathlib import Path
|
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
|
|
|
|
OUT = Path("docs/penjelasan-lengkap-kmeans-rekomendasi-budget.docx")
|
|
|
|
|
|
def paragraph(text="", style=None):
|
|
text = escape(text)
|
|
style_xml = f'<w:pPr><w:pStyle w:val="{style}"/></w:pPr>' if style else ""
|
|
return f'<w:p>{style_xml}<w:r><w:t xml:space="preserve">{text}</w:t></w:r></w:p>'
|
|
|
|
|
|
def code_block(text):
|
|
rows = []
|
|
for line in text.strip("\n").splitlines():
|
|
rows.append(
|
|
'<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr>'
|
|
f'<w:r><w:t xml:space="preserve">{escape(line)}</w:t></w:r></w:p>'
|
|
)
|
|
return "".join(rows)
|
|
|
|
|
|
def table(headers, rows):
|
|
xml = [
|
|
'<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/>'
|
|
'<w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
'<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
'<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
'<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
'<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
'<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
|
|
"</w:tblBorders></w:tblPr>"
|
|
]
|
|
for row in [headers] + rows:
|
|
xml.append("<w:tr>")
|
|
for cell in row:
|
|
xml.append('<w:tc><w:tcPr><w:tcW w:w="2500" w:type="dxa"/></w:tcPr>')
|
|
xml.append(paragraph(str(cell)))
|
|
xml.append("</w:tc>")
|
|
xml.append("</w:tr>")
|
|
xml.append("</w:tbl>")
|
|
return "".join(xml)
|
|
|
|
|
|
def bullet(items):
|
|
return "".join(paragraph(f"- {item}") for item in items)
|
|
|
|
|
|
def build_body():
|
|
body = []
|
|
|
|
body.append(paragraph("Penjelasan Lengkap Metode K-Means pada Fitur Rekomendasi Mobil Berdasarkan Budget", "Title"))
|
|
body.append(paragraph(
|
|
"Dokumen ini menjelaskan tahapan metode K-Means yang digunakan pada fitur rekomendasi mobil sesuai budget. "
|
|
"Penjelasan disesuaikan dengan implementasi sistem, yaitu aplikasi Laravel sebagai sistem utama dan Flask API "
|
|
"sebagai layanan pengolahan rekomendasi."
|
|
))
|
|
|
|
body.append(paragraph("1. Gambaran Umum Fitur Rekomendasi", "Heading1"))
|
|
body.append(paragraph(
|
|
"Fitur rekomendasi budget bekerja ketika pengguna memasukkan nominal budget maksimal. Laravel menerima input "
|
|
"tersebut, lalu mengirim request ke Flask API. Flask API membaca dataset mobil, menjalankan proses K-Means, "
|
|
"memilih cluster yang paling sesuai dengan budget pengguna, kemudian mengembalikan daftar mobil rekomendasi "
|
|
"kepada Laravel."
|
|
))
|
|
body.append(paragraph("Alur sistem secara ringkas:", "Heading2"))
|
|
body.append(bullet([
|
|
"Pengguna memasukkan budget pada halaman rekomendasi.",
|
|
"Laravel memanggil endpoint Flask API /recommend.",
|
|
"Flask membaca dataset mobil.",
|
|
"Flask membentuk cluster dengan K-Means berdasarkan harga, tahun, dan mesin_cc.",
|
|
"Sistem menentukan cluster yang paling dekat dengan budget pengguna.",
|
|
"Mobil dalam cluster tersebut difilter agar tidak melebihi budget.",
|
|
"Hasil akhir diurutkan menggunakan scoring, lalu ditampilkan oleh Laravel."
|
|
]))
|
|
|
|
body.append(paragraph("2. Atribut yang Digunakan dalam K-Means", "Heading1"))
|
|
body.append(paragraph(
|
|
"K-Means pada sistem ini menggunakan tiga atribut numerik. Ketiga atribut tersebut diambil dari data mobil "
|
|
"dan digunakan sebagai vector fitur untuk proses clustering."
|
|
))
|
|
body.append(table(["Atribut", "Nama di Kode", "Fungsi dalam Rekomendasi"], [
|
|
["Harga mobil", "harga", "Menentukan kedekatan mobil dengan budget pengguna."],
|
|
["Tahun mobil", "tahun", "Menunjukkan usia kendaraan; mobil lebih muda biasanya lebih menarik."],
|
|
["Kapasitas mesin", "mesin_cc", "Menunjukkan karakteristik performa dan spesifikasi kendaraan."],
|
|
]))
|
|
body.append(paragraph("Potongan kode atribut yang digunakan:", "Heading2"))
|
|
body.append(code_block("""
|
|
def car_features(car):
|
|
return [car["harga"], car["tahun"], car["mesin_cc"]]
|
|
"""))
|
|
body.append(paragraph(
|
|
"Kode tersebut menunjukkan bahwa setiap mobil diubah menjadi vector berisi tiga nilai, yaitu harga, tahun, "
|
|
"dan kapasitas mesin. Vector inilah yang kemudian diproses dalam K-Means."
|
|
))
|
|
|
|
body.append(paragraph("3. Membaca dan Menyiapkan Dataset", "Heading1"))
|
|
body.append(paragraph(
|
|
"Flask API membaca dataset mobil dari file yang tersedia di project. Setiap baris dataset diubah menjadi data "
|
|
"mobil yang memiliki atribut merk, model, kategori, tahun, mesin_cc, transmisi, bahan_bakar, harga, dan cluster. "
|
|
"Untuk proses K-Means, atribut yang digunakan hanya harga, tahun, dan mesin_cc."
|
|
))
|
|
body.append(paragraph("Potongan kode pembacaan data:", "Heading2"))
|
|
body.append(code_block("""
|
|
cars.append(
|
|
{
|
|
"merk": row.get("merk", "").strip(),
|
|
"model": row.get("model", "").strip(),
|
|
"nama": f"{row.get('merk', '').strip()} {row.get('model', '').strip()}".strip(),
|
|
"tahun": to_int(row.get("tahun")),
|
|
"mesin_cc": to_int(row.get("mesin_cc")),
|
|
"harga": price,
|
|
"cluster": to_int(row.get("cluster"), default=None),
|
|
}
|
|
)
|
|
"""))
|
|
body.append(paragraph(
|
|
"Data dengan harga tidak valid tidak digunakan. Pada pembentukan model K-Means, sistem juga hanya memakai data "
|
|
"yang memiliki harga, tahun, dan mesin_cc lebih dari 0."
|
|
))
|
|
|
|
body.append(paragraph("4. Normalisasi Data", "Heading1"))
|
|
body.append(paragraph(
|
|
"Normalisasi adalah proses mengubah nilai atribut ke skala yang sama. Pada sistem ini digunakan Min-Max "
|
|
"Normalization agar nilai berada pada rentang 0 sampai 1. Normalisasi diperlukan karena harga memiliki skala "
|
|
"ratusan juta, sedangkan tahun sekitar 2019-2025 dan mesin sekitar 1000-2500 cc. Tanpa normalisasi, atribut "
|
|
"harga akan terlalu mendominasi perhitungan jarak."
|
|
))
|
|
body.append(paragraph("Rumus normalisasi:", "Heading2"))
|
|
body.append(code_block("""
|
|
nilai_normal = (nilai - nilai_minimum) / (nilai_maksimum - nilai_minimum)
|
|
"""))
|
|
body.append(paragraph("Potongan kode normalisasi:", "Heading2"))
|
|
body.append(code_block("""
|
|
def scaled_value(value, minimum, maximum):
|
|
if maximum == minimum:
|
|
return 0.0
|
|
|
|
return (value - minimum) / (maximum - minimum)
|
|
"""))
|
|
body.append(paragraph("Kode pembentukan vector normalisasi:", "Heading2"))
|
|
body.append(code_block("""
|
|
feature_rows = [car_features(car) for car in usable_cars]
|
|
columns = list(zip(*feature_rows))
|
|
ranges = [(min(column), max(column)) for column in columns]
|
|
vectors = [
|
|
[scaled_value(value, ranges[index][0], ranges[index][1]) for index, value in enumerate(row)]
|
|
for row in feature_rows
|
|
]
|
|
"""))
|
|
|
|
body.append(paragraph("5. Contoh Data dan Perhitungan Normalisasi", "Heading1"))
|
|
body.append(paragraph("Misalkan terdapat data mobil berikut:"))
|
|
body.append(table(["Mobil", "Harga", "Tahun", "Mesin CC"], [
|
|
["Toyota Avanza", "125.000.000", "2019", "1500"],
|
|
["Hyundai Ioniq", "122.000.000", "2021", "1197"],
|
|
["Toyota Fortuner", "200.000.000", "2020", "2500"],
|
|
["Toyota Innova", "250.000.000", "2020", "2000"],
|
|
["Fortuner Baru", "500.000.000", "2021", "2500"],
|
|
]))
|
|
body.append(paragraph("Nilai minimum dan maksimum dari contoh data:", "Heading2"))
|
|
body.append(table(["Atribut", "Minimum", "Maksimum"], [
|
|
["Harga", "122.000.000", "500.000.000"],
|
|
["Tahun", "2019", "2021"],
|
|
["Mesin CC", "1197", "2500"],
|
|
]))
|
|
body.append(paragraph("Contoh normalisasi Toyota Avanza:", "Heading2"))
|
|
body.append(code_block("""
|
|
Harga:
|
|
(125.000.000 - 122.000.000) / (500.000.000 - 122.000.000)
|
|
= 3.000.000 / 378.000.000
|
|
= 0,008
|
|
|
|
Tahun:
|
|
(2019 - 2019) / (2021 - 2019)
|
|
= 0 / 2
|
|
= 0
|
|
|
|
Mesin:
|
|
(1500 - 1197) / (2500 - 1197)
|
|
= 303 / 1303
|
|
= 0,233
|
|
"""))
|
|
body.append(paragraph("Jadi vector normalisasi Toyota Avanza adalah:", "Heading2"))
|
|
body.append(code_block("""
|
|
[0,008, 0,000, 0,233]
|
|
"""))
|
|
body.append(paragraph("Contoh normalisasi Toyota Fortuner harga 200 juta:", "Heading2"))
|
|
body.append(code_block("""
|
|
Harga:
|
|
(200.000.000 - 122.000.000) / (500.000.000 - 122.000.000)
|
|
= 78.000.000 / 378.000.000
|
|
= 0,206
|
|
|
|
Tahun:
|
|
(2020 - 2019) / (2021 - 2019)
|
|
= 1 / 2
|
|
= 0,500
|
|
|
|
Mesin:
|
|
(2500 - 1197) / (2500 - 1197)
|
|
= 1303 / 1303
|
|
= 1,000
|
|
"""))
|
|
body.append(paragraph("Jadi vector normalisasi Toyota Fortuner adalah:", "Heading2"))
|
|
body.append(code_block("""
|
|
[0,206, 0,500, 1,000]
|
|
"""))
|
|
|
|
body.append(paragraph("6. Menentukan Centroid Awal", "Heading1"))
|
|
body.append(paragraph(
|
|
"Centroid adalah titik pusat cluster. Pada implementasi sistem, centroid awal dipilih berdasarkan data yang "
|
|
"sudah diurutkan menurut harga. Cara ini membuat centroid awal tersebar dari data harga rendah sampai harga "
|
|
"tinggi. Jumlah cluster default yang digunakan adalah 4 cluster."
|
|
))
|
|
body.append(paragraph("Potongan kode centroid awal:", "Heading2"))
|
|
body.append(code_block("""
|
|
cluster_count = max(1, min(cluster_count, len(vectors)))
|
|
sorted_indexes = sorted(range(len(usable_cars)), key=lambda index: usable_cars[index]["harga"])
|
|
centroids = []
|
|
|
|
for cluster_index in range(cluster_count):
|
|
sample_position = round(cluster_index * (len(sorted_indexes) - 1) / max(cluster_count - 1, 1))
|
|
centroids.append(vectors[sorted_indexes[sample_position]][:])
|
|
"""))
|
|
body.append(paragraph(
|
|
"Secara teori, centroid awal dapat ditentukan secara acak atau menggunakan strategi tertentu. Pada sistem ini, "
|
|
"centroid awal tidak dipilih acak, melainkan berdasarkan persebaran harga agar cluster awal lebih stabil."
|
|
))
|
|
|
|
body.append(paragraph("7. Menghitung Jarak dengan Euclidean Distance", "Heading1"))
|
|
body.append(paragraph(
|
|
"Setelah centroid awal ditentukan, sistem menghitung jarak setiap mobil terhadap seluruh centroid. Jarak yang "
|
|
"digunakan adalah Euclidean Distance."
|
|
))
|
|
body.append(paragraph("Rumus Euclidean Distance:", "Heading2"))
|
|
body.append(code_block("""
|
|
d = sqrt((x1 - c1)^2 + (x2 - c2)^2 + (x3 - c3)^2)
|
|
"""))
|
|
body.append(paragraph("Keterangan:", "Heading2"))
|
|
body.append(bullet([
|
|
"x1, x2, x3 adalah nilai normalisasi mobil untuk harga, tahun, dan mesin_cc.",
|
|
"c1, c2, c3 adalah nilai centroid untuk harga, tahun, dan mesin_cc.",
|
|
"d adalah jarak antara mobil dengan centroid."
|
|
]))
|
|
body.append(paragraph("Potongan kode jarak:", "Heading2"))
|
|
body.append(code_block("""
|
|
def distance(point, centroid):
|
|
return math.sqrt(sum((point[index] - centroid[index]) ** 2 for index in range(len(point))))
|
|
"""))
|
|
body.append(paragraph("Contoh perhitungan jarak:", "Heading2"))
|
|
body.append(paragraph(
|
|
"Misalnya vector normalisasi Toyota Avanza adalah [0,008, 0,000, 0,233] dan centroid C1 adalah "
|
|
"[0,000, 1,000, 0,000]. Maka jaraknya:"
|
|
))
|
|
body.append(code_block("""
|
|
d = sqrt((0,008 - 0,000)^2 + (0,000 - 1,000)^2 + (0,233 - 0,000)^2)
|
|
d = sqrt(0,000064 + 1,000000 + 0,054289)
|
|
d = sqrt(1,054353)
|
|
d = 1,027
|
|
"""))
|
|
body.append(paragraph(
|
|
"Jarak yang lebih kecil menunjukkan bahwa data mobil lebih dekat dengan centroid tersebut. Mobil akan masuk "
|
|
"ke cluster dengan centroid yang jaraknya paling kecil."
|
|
))
|
|
|
|
body.append(paragraph("8. Menentukan Cluster Terdekat", "Heading1"))
|
|
body.append(paragraph(
|
|
"Setelah jarak dihitung, sistem memasukkan setiap mobil ke cluster dengan centroid terdekat. Tahap ini disebut "
|
|
"assignment step pada K-Means."
|
|
))
|
|
body.append(paragraph("Potongan kode assignment cluster:", "Heading2"))
|
|
body.append(code_block("""
|
|
for index, vector in enumerate(vectors):
|
|
nearest_cluster = min(range(cluster_count), key=lambda cluster: distance(vector, centroids[cluster]))
|
|
if labels[index] != nearest_cluster:
|
|
changed = True
|
|
labels[index] = nearest_cluster
|
|
"""))
|
|
body.append(paragraph(
|
|
"Variabel labels menyimpan nomor cluster untuk setiap mobil. Jika cluster sebuah mobil berubah, variabel "
|
|
"changed bernilai True, sehingga proses iterasi akan berlanjut."
|
|
))
|
|
|
|
body.append(paragraph("9. Menghitung Ulang Centroid", "Heading1"))
|
|
body.append(paragraph(
|
|
"Setelah semua mobil ditempatkan pada cluster terdekat, sistem menghitung ulang centroid. Centroid baru "
|
|
"didapatkan dari rata-rata seluruh anggota dalam cluster tersebut."
|
|
))
|
|
body.append(paragraph("Potongan kode update centroid:", "Heading2"))
|
|
body.append(code_block("""
|
|
new_centroids = []
|
|
for cluster in range(cluster_count):
|
|
members = [vectors[index] for index, label in enumerate(labels) if label == cluster]
|
|
if not members:
|
|
new_centroids.append(centroids[cluster])
|
|
continue
|
|
|
|
new_centroids.append(
|
|
[sum(member[column] for member in members) / len(members) for column in range(len(vectors[0]))]
|
|
)
|
|
|
|
centroids = new_centroids
|
|
"""))
|
|
body.append(paragraph("Contoh perhitungan centroid baru:", "Heading2"))
|
|
body.append(paragraph(
|
|
"Misalnya Cluster 0 memiliki dua anggota dengan vector normalisasi berikut:"
|
|
))
|
|
body.append(code_block("""
|
|
Mobil A = [0,008, 0,000, 0,233]
|
|
Mobil B = [0,000, 1,000, 0,000]
|
|
"""))
|
|
body.append(paragraph("Centroid baru Cluster 0 dihitung dengan rata-rata setiap atribut:", "Heading2"))
|
|
body.append(code_block("""
|
|
Harga centroid = (0,008 + 0,000) / 2 = 0,004
|
|
Tahun centroid = (0,000 + 1,000) / 2 = 0,500
|
|
Mesin centroid = (0,233 + 0,000) / 2 = 0,1165
|
|
|
|
Centroid baru = [0,004, 0,500, 0,1165]
|
|
"""))
|
|
|
|
body.append(paragraph("10. Iterasi Sampai Stabil", "Heading1"))
|
|
body.append(paragraph(
|
|
"Proses menghitung jarak, menentukan cluster, dan menghitung ulang centroid dilakukan berulang. Iterasi akan "
|
|
"berhenti jika anggota cluster tidak berubah lagi atau jika sudah mencapai batas iterasi. Dalam sistem ini, "
|
|
"batas iterasi default adalah 30."
|
|
))
|
|
body.append(paragraph("Potongan kode iterasi:", "Heading2"))
|
|
body.append(code_block("""
|
|
for _ in range(iterations):
|
|
changed = False
|
|
|
|
# assignment step
|
|
# update centroid step
|
|
|
|
if not changed:
|
|
break
|
|
"""))
|
|
|
|
body.append(paragraph("11. Menentukan Cluster yang Sesuai dengan Budget User", "Heading1"))
|
|
body.append(paragraph(
|
|
"Setelah model K-Means terbentuk, sistem harus menentukan cluster mana yang paling cocok dengan budget "
|
|
"pengguna. Untuk itu, sistem membuat data target dari budget pengguna. Nilai harga target adalah budget yang "
|
|
"dimasukkan user, sedangkan tahun dan mesin target diambil dari median mobil yang harganya kurang dari atau "
|
|
"sama dengan budget."
|
|
))
|
|
body.append(paragraph("Potongan kode penentuan cluster budget:", "Heading2"))
|
|
body.append(code_block("""
|
|
def predict_budget_cluster(model, budget):
|
|
model_cars = model["cars"]
|
|
target_year = median([car["tahun"] for car in model_cars if car["harga"] <= budget] or [car["tahun"] for car in model_cars])
|
|
target_engine = median([car["mesin_cc"] for car in model_cars if car["harga"] <= budget] or [car["mesin_cc"] for car in model_cars])
|
|
target_features = [budget, target_year, target_engine]
|
|
target_vector = [
|
|
scaled_value(value, model["ranges"][index][0], model["ranges"][index][1])
|
|
for index, value in enumerate(target_features)
|
|
]
|
|
|
|
return min(
|
|
range(model["cluster_count"]),
|
|
key=lambda cluster: distance(target_vector, model["centroids"][cluster]),
|
|
)
|
|
"""))
|
|
body.append(paragraph("Contoh:", "Heading2"))
|
|
body.append(paragraph(
|
|
"Jika pengguna memasukkan budget Rp200.000.000, sistem membuat target harga 200.000.000. Tahun dan mesin "
|
|
"target dihitung dari median data mobil yang harganya <= 200.000.000. Setelah itu target dinormalisasi dan "
|
|
"dibandingkan dengan semua centroid. Cluster dengan jarak paling kecil menjadi cluster rekomendasi."
|
|
))
|
|
|
|
body.append(paragraph("12. Filter Mobil Berdasarkan Cluster dan Budget", "Heading1"))
|
|
body.append(paragraph(
|
|
"Setelah cluster target ditemukan, sistem mengambil mobil dari cluster tersebut dengan syarat harga tidak "
|
|
"melebihi budget. Jika ada mobil dengan harga sama persis dengan budget, mobil tersebut diprioritaskan."
|
|
))
|
|
body.append(paragraph("Potongan kode filter budget:", "Heading2"))
|
|
body.append(code_block("""
|
|
same_cluster = [
|
|
car for car in cars
|
|
if model["labels"].get(id(car)) == target_cluster and car["harga"] <= budget
|
|
]
|
|
|
|
exact_matches = [car for car in same_cluster if car["harga"] == budget]
|
|
selected = exact_matches or same_cluster or [car for car in cars if car["harga"] <= budget]
|
|
"""))
|
|
body.append(paragraph("Contoh jika budget Rp200.000.000:", "Heading2"))
|
|
body.append(table(["Mobil", "Harga", "Keterangan"], [
|
|
["Toyota Avanza", "125.000.000", "Lolos karena di bawah budget"],
|
|
["Hyundai Ioniq", "122.000.000", "Lolos karena di bawah budget"],
|
|
["Toyota Fortuner", "200.000.000", "Diprioritaskan karena sama dengan budget"],
|
|
["Toyota Innova", "250.000.000", "Tidak lolos karena melebihi budget"],
|
|
["Fortuner Baru", "500.000.000", "Tidak lolos karena melebihi budget"],
|
|
]))
|
|
|
|
body.append(paragraph("13. Scoring Setelah K-Means", "Heading1"))
|
|
body.append(paragraph(
|
|
"K-Means digunakan untuk membentuk kelompok kandidat mobil. Setelah kandidat diperoleh, sistem masih melakukan "
|
|
"scoring untuk mengurutkan hasil rekomendasi. Scoring ini bukan proses K-Means, melainkan proses tambahan agar "
|
|
"hasil rekomendasi lebih rapi dan relevan."
|
|
))
|
|
body.append(paragraph("Potongan kode bobot strategi harga_pas:", "Heading2"))
|
|
body.append(code_block("""
|
|
if normalize_text(strategy) in ["harga_pas", "harga pas", "spesifik", "specific"]:
|
|
return {
|
|
"budget": 0.70,
|
|
"preference": 0.15,
|
|
"year": 0.10,
|
|
"engine": 0.05,
|
|
"cluster": 0.00,
|
|
}
|
|
"""))
|
|
body.append(paragraph(
|
|
"Pada strategi harga_pas, bobot harga adalah 70%. Artinya hasil rekomendasi lebih mengutamakan mobil yang "
|
|
"harganya paling mendekati budget pengguna. Tahun dan mesin tetap dihitung, tetapi pengaruhnya lebih kecil."
|
|
))
|
|
|
|
body.append(paragraph("14. Kesimpulan Tahapan K-Means pada Sistem", "Heading1"))
|
|
body.append(paragraph(
|
|
"Tahapan K-Means pada sistem rekomendasi mobil sudah sesuai dengan konsep dasar K-Means. Sistem menentukan "
|
|
"atribut, melakukan normalisasi, menentukan centroid awal, menghitung jarak, membentuk cluster, memperbarui "
|
|
"centroid, dan mengulang proses sampai stabil. Setelah itu, hasil clustering digunakan untuk memilih mobil "
|
|
"yang paling sesuai dengan budget pengguna."
|
|
))
|
|
body.append(paragraph("Kalimat yang dapat digunakan dalam laporan:", "Heading2"))
|
|
body.append(paragraph(
|
|
"Pada sistem ini, metode K-Means digunakan untuk mengelompokkan data mobil berdasarkan tiga atribut, yaitu "
|
|
"harga, tahun, dan kapasitas mesin. Data terlebih dahulu dinormalisasi menggunakan Min-Max Normalization agar "
|
|
"setiap atribut berada pada skala yang sama. Setelah itu, sistem membentuk cluster menggunakan perhitungan "
|
|
"jarak Euclidean. Cluster yang paling dekat dengan budget pengguna dipilih sebagai dasar rekomendasi. Mobil "
|
|
"dalam cluster tersebut kemudian disaring agar tidak melebihi budget dan diurutkan menggunakan scoring."
|
|
))
|
|
|
|
body.append(paragraph("15. Catatan Penting untuk Demo dan Sidang", "Heading1"))
|
|
body.append(paragraph(
|
|
"K-Means pada sistem dijalankan oleh Flask API melalui file flask_api/app.py. Laravel hanya memanggil hasil "
|
|
"rekomendasi dari API tersebut. Jika Flask API tidak aktif, Laravel akan memakai fallback filter harga biasa. "
|
|
"Oleh karena itu, saat demo atau sidang, Flask API harus dijalankan agar fitur rekomendasi benar-benar memakai "
|
|
"K-Means."
|
|
))
|
|
|
|
body.append(paragraph("16. Kode Program Clustering dengan 3 Atribut", "Heading1"))
|
|
body.append(paragraph(
|
|
"Berikut adalah kode program Python untuk proses clustering K-Means yang disesuaikan dengan tiga atribut utama "
|
|
"pada sistem rekomendasi, yaitu harga, tahun, dan mesin_cc. Kode ini dapat digunakan sebagai contoh implementasi "
|
|
"pada laporan karena hanya memakai atribut yang relevan dengan fitur rekomendasi budget."
|
|
))
|
|
body.append(code_block("""
|
|
# Import library
|
|
import pandas as pd
|
|
|
|
from sklearn.preprocessing import StandardScaler
|
|
from sklearn.cluster import KMeans
|
|
from sklearn.metrics import silhouette_score
|
|
|
|
|
|
# Membaca dataset
|
|
df = pd.read_csv("dataset_final.csv")
|
|
|
|
|
|
# Jika masih ada kolom cluster lama, hapus terlebih dahulu
|
|
if "cluster" in df.columns:
|
|
df = df.drop(columns=["cluster"])
|
|
|
|
|
|
# Samakan nama kolom harga
|
|
if "Harga" in df.columns:
|
|
df = df.rename(columns={"Harga": "harga"})
|
|
|
|
|
|
# Pilih 3 atribut utama untuk clustering
|
|
fitur = df[
|
|
[
|
|
"harga",
|
|
"tahun",
|
|
"mesin_cc"
|
|
]
|
|
]
|
|
|
|
|
|
# Standarisasi data
|
|
scaler = StandardScaler()
|
|
fitur_scaled = scaler.fit_transform(fitur)
|
|
|
|
|
|
# Mencari jumlah cluster terbaik menggunakan silhouette score
|
|
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}")
|
|
|
|
|
|
# Membuat model K-Means akhir
|
|
kmeans = KMeans(
|
|
n_clusters=4,
|
|
random_state=42,
|
|
n_init=10
|
|
)
|
|
|
|
|
|
# Menambahkan hasil cluster ke dataset
|
|
df["cluster"] = kmeans.fit_predict(fitur_scaled)
|
|
|
|
|
|
# Menampilkan hasil clustering
|
|
print(
|
|
df[
|
|
[
|
|
"merk",
|
|
"model",
|
|
"harga",
|
|
"tahun",
|
|
"mesin_cc",
|
|
"cluster"
|
|
]
|
|
].head(20)
|
|
)
|
|
|
|
|
|
# Menyimpan dataset yang sudah memiliki cluster
|
|
df.to_csv("dataset_final_clustered.csv", index=False)
|
|
"""))
|
|
body.append(paragraph(
|
|
"Pada kode tersebut, data terlebih dahulu dibaca dari file dataset_final.csv. Jika terdapat kolom cluster lama, "
|
|
"kolom tersebut dihapus agar hasil clustering dihitung ulang dari awal. Setelah itu, sistem memilih tiga atribut "
|
|
"utama, yaitu harga, tahun, dan mesin_cc."
|
|
))
|
|
body.append(paragraph(
|
|
"Tahap standarisasi dilakukan menggunakan StandardScaler agar ketiga atribut berada pada skala yang seimbang. "
|
|
"Hal ini penting karena harga memiliki nilai yang jauh lebih besar dibandingkan tahun dan mesin_cc. Setelah data "
|
|
"distandarisasi, K-Means dijalankan untuk membentuk cluster."
|
|
))
|
|
body.append(paragraph(
|
|
"Bagian silhouette score digunakan untuk melihat kualitas beberapa jumlah cluster. Setelah itu, sistem membuat "
|
|
"model akhir. Pada contoh ini digunakan 4 cluster agar sesuai dengan konfigurasi sistem rekomendasi yang mengirim "
|
|
"parameter jumlah_cluster = 4 ke Flask API."
|
|
))
|
|
|
|
return "".join(body)
|
|
|
|
|
|
def write_docx():
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
|
</Types>"""
|
|
|
|
rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
</Relationships>"""
|
|
|
|
doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>"""
|
|
|
|
styles = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/><w:rPr><w:sz w:val="22"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:b/><w:sz w:val="26"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:b/><w:sz w:val="23"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/><w:sz w:val="19"/></w:rPr></w:style>
|
|
<w:style w:type="table" w:styleId="TableGrid"><w:name w:val="Table Grid"/><w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr></w:style>
|
|
</w:styles>"""
|
|
|
|
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:body>
|
|
{build_body()}
|
|
<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr>
|
|
</w:body>
|
|
</w:document>"""
|
|
|
|
with ZipFile(OUT, "w", ZIP_DEFLATED) as docx:
|
|
docx.writestr("[Content_Types].xml", content_types)
|
|
docx.writestr("_rels/.rels", rels)
|
|
docx.writestr("word/_rels/document.xml.rels", doc_rels)
|
|
docx.writestr("word/styles.xml", styles)
|
|
docx.writestr("word/document.xml", document)
|
|
|
|
print(OUT.resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
write_docx()
|