TA/web_JMW/docs/build_kmeans_detailed_doc.py

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()