{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### UJIAN TENGAH SEMESTER — BIG DATA\n\n**Analisis Data Transaksi Ritel Fashion H&M Menggunakan Pipeline ETL Apache Spark dan Spark SQL**\n\n---\n\n#### Gambaran Umum Proyek\n\nProyek ini bertujuan mengolah dan menganalisis data transaksi ritel fashion menggunakan Apache Spark melalui pendekatan *Extract, Transform, Load* (ETL). Dataset yang digunakan adalah **H&M Personalized Fashion Recommendations**, yang memuat riwayat pembelian pelanggan, informasi produk, serta karakteristik pelanggan.\n\nPengolahan data dilakukan menggunakan **PySpark** untuk membaca, memeriksa, membersihkan, mentransformasi, dan menggabungkan data dari beberapa tabel. Hasil transformasi disimpan dalam format **Parquet**, kemudian dianalisis menggunakan **Spark DataFrame dan Spark SQL** untuk memperoleh informasi mengenai kontribusi kelompok produk, perkembangan transaksi bulanan, serta perbandingan saluran penjualan.\n\nProyek ini juga mencakup penerapan perlindungan data pelanggan, evaluasi kualitas data, dan penyusunan rekomendasi berdasarkan hasil analisis.\n\n#### Informasi Dataset dan Teknologi\n\n| Komponen | Keterangan |\n|---|---|\n| Dataset | H&M Personalized Fashion Recommendations |\n| Sumber | H&M Group melalui Kaggle |\n| Bidang | Ritel Fashion |\n| Data Utama | `transactions_train.csv` |\n| Data Pendukung | `articles.csv` dan `customers.csv` |\n| Lingkungan Pengolahan | Kaggle Notebook |\n| Bahasa Pemrograman | Python |\n| Teknologi Pengolahan | Apache Spark (PySpark) |\n| Analisis | Spark DataFrame dan Spark SQL |\n| Format Penyimpanan Hasil | Parquet |\n\n**Sumber dataset:** [H&M Personalized Fashion Recommendations — Kaggle](https://www.kaggle.com/competitions/h-and-m-personalized-fashion-recommendations/data)\n\n---\n\n*Notebook ini disusun berdasarkan tujuh tahapan UTS Big Data, meliputi perumusan pertanyaan bisnis, identifikasi jenis data dan analitik, proses ETL, analisis menggunakan Spark SQL, serta penerapan etika, privasi, dan tata kelola data. Hasil pengolahan digunakan untuk menyusun insight, rekomendasi, dan keterbatasan analisis.*","metadata":{}},{"cell_type":"markdown","source":"## 1. Pertanyaan Bisnis dan Karakteristik Data\n\n#### 1.1 Konteks Bisnis dan Pengambil Keputusan\n\nH&M merupakan perusahaan ritel fashion yang menyediakan berbagai produk pakaian dan aksesori melalui saluran penjualan fisik maupun daring. Dataset *H&M Personalized Fashion Recommendations* memuat riwayat pembelian pelanggan yang dilengkapi dengan informasi produk dan karakteristik pelanggan. Melalui data tersebut, perusahaan dapat mengevaluasi kontribusi kelompok produk terhadap nilai transaksi, mengidentifikasi perkembangan aktivitas pembelian berdasarkan waktu, serta membandingkan kinerja saluran penjualan. Dalam proyek ini, data diolah menggunakan Apache Spark untuk menghasilkan informasi yang dapat mendukung **manajer penjualan, manajer produk, dan manajer operasional H&M** sebagai pengambil keputusan dalam menentukan prioritas pengelolaan produk, mengevaluasi perubahan aktivitas penjualan, dan merencanakan strategi pengembangan saluran penjualan.\n\n#### 1.2 Pertanyaan Bisnis\n\nBerdasarkan karakteristik data yang tersedia, dirumuskan tiga pertanyaan bisnis yang akan dijawab menggunakan Spark SQL pada Tahap 6.\n\n1. **Kelompok produk apa yang memberikan kontribusi total nilai transaksi terbesar selama periode pengamatan?**\n\n   Pertanyaan ini dijawab dengan menggabungkan tabel `transactions_train.csv` dan `articles.csv` melalui `article_id`, kemudian menghitung total nilai `price` berdasarkan `product_group_name`.\n\n2. **Bagaimana perkembangan jumlah pembelian produk dan total nilai transaksi dari bulan ke bulan selama periode pengamatan?**\n\n   Pertanyaan ini dijawab dengan mengelompokkan catatan pembelian berdasarkan bulan pada kolom `t_dat`, kemudian menghitung jumlah pembelian produk dan total nilai `price` pada setiap bulan.\n\n3. **Bagaimana perbandingan jumlah pembelian produk dan total nilai transaksi berdasarkan saluran penjualan fisik dan daring?**\n\n   Pertanyaan ini dijawab dengan mengelompokkan catatan pembelian berdasarkan `sales_channel_id`, kemudian membandingkan jumlah pembelian produk dan total nilai `price` pada setiap saluran penjualan.\n\nKetiga pertanyaan tersebut menggunakan pendekatan analitik deskriptif karena berfokus pada penggambaran aktivitas transaksi yang telah terjadi.\n\n**Catatan:** Setiap baris pada tabel transaksi merepresentasikan satu catatan pembelian produk, bukan selalu satu pesanan unik. Selain itu, `price` merupakan nilai harga yang telah dinormalisasi sehingga hasil penjumlahannya tidak diinterpretasikan sebagai nominal mata uang tertentu.\n\n#### 1.3 Karakteristik 5V Big Data\n\nKarakteristik dataset H&M dijelaskan berdasarkan lima dimensi Big Data, yaitu *Volume, Velocity, Variety, Veracity,* dan *Value*.\n\n##### A. Volume\n\nDataset H&M memiliki **31.788.324 catatan pembelian** pada tabel transaksi utama, yang didukung oleh 105.542 data produk dan 1.371.980 data pelanggan. Secara keseluruhan, ketiga tabel memiliki 33.265.846 baris dengan total ukuran berkas sebesar 3.558,41 MB atau sekitar 3,48 GB.\n\n| Sumber Data | Jumlah Baris | Ukuran Berkas |\n|---|---:|---:|\n| `transactions_train.csv` | 31.788.324 | 3.326,42 MB |\n| `articles.csv` | 105.542 | 34,45 MB |\n| `customers.csv` | 1.371.980 | 197,54 MB |\n| **Total** | **33.265.846** | **3.558,41 MB** |\n\nJumlah baris diperoleh menggunakan fungsi `count()` pada Spark DataFrame, sedangkan ukuran berkas dihitung berdasarkan metadata file CSV. Dengan jumlah baris tabel utama yang melebihi satu juta dan total ukuran berkas di atas 100 MB, dataset H&M memenuhi persyaratan volume data pada proyek UTS Big Data.\n\n##### B. Velocity\n\nDataset H&M mencatat aktivitas pembelian pelanggan berdasarkan tanggal pada kolom `t_dat`. Data transaksi mencakup periode **20 September 2018 hingga 22 September 2020**, dengan aktivitas pembelian yang tercatat pada 734 hari. Rata-rata terdapat 43.308,34 catatan pembelian per hari, sedangkan jumlah pembelian harian tertinggi mencapai 198.622 catatan.\n\n| Indikator | Hasil Pemeriksaan |\n|---|---:|\n| Tanggal transaksi paling awal | 20 September 2018 |\n| Tanggal transaksi paling akhir | 22 September 2020 |\n| Jumlah hari dengan transaksi | 734 hari |\n| Rata-rata catatan pembelian per hari | 43.308,34 |\n| Jumlah catatan pembelian tertinggi dalam satu hari | 198.622 |\n\nPencatatan transaksi harian memungkinkan analisis perubahan aktivitas pembelian dari waktu ke waktu, termasuk perkembangan transaksi bulanan. Meskipun data memiliki frekuensi pencatatan harian, dataset ini merupakan data historis dan tidak diproses sebagai aliran data *real-time*.\n\n##### C. Variety\n\nDataset H&M terdiri atas tiga sumber data terstruktur dengan atribut yang mencakup informasi transaksi, produk, dan pelanggan.\n\n| Sumber Data | Variabel | Karakteristik Informasi |\n|---|---|---|\n| `transactions_train.csv` | `t_dat`, `customer_id`, `article_id`, `price`, `sales_channel_id` | Tanggal pembelian, identitas pelanggan dan produk, nilai harga, serta kategori saluran penjualan |\n| `articles.csv` | `prod_name`, `product_type_name`, `product_group_name`, `colour_group_name` | Nama, jenis, kelompok, dan warna produk |\n| `customers.csv` | `age`, `club_member_status`, `fashion_news_frequency`, `postal_code` | Usia, status keanggotaan, frekuensi komunikasi, dan informasi kode pos pelanggan |\n\nKetiga sumber data dapat dihubungkan melalui `article_id` dan `customer_id`, sehingga analisis transaksi dapat dilengkapi dengan karakteristik produk dan pelanggan.\n\n##### D. Veracity\n\nPemeriksaan kualitas data H&M difokuskan pada kelengkapan nilai, validitas atribut transaksi, serta konsistensi hubungan antartabel.\n\n| Aspek Pemeriksaan | Variabel | Pemeriksaan yang Dilakukan |\n|---|---|---|\n| Nilai kosong | `age`, `price`, `customer_id`, `article_id` | Menghitung jumlah nilai `null` atau kosong |\n| Validitas tanggal | `t_dat` | Memeriksa tanggal yang tidak dapat dikonversi |\n| Kewajaran harga | `price` | Mengidentifikasi nilai negatif atau tidak valid |\n| Konsistensi kategori | `sales_channel_id` | Memeriksa kode saluran penjualan yang tersedia |\n| Konsistensi relasi | `article_id`, `customer_id` | Memeriksa kecocokan kunci antartabel |\n| Duplikasi transaksi | Seluruh kolom transaksi | Mengidentifikasi baris identik tanpa langsung menghapusnya |\n\nBaris transaksi yang identik pada dataset H&M dapat merepresentasikan pembelian berulang atas produk yang sama, sehingga tidak otomatis dianggap sebagai duplikasi yang harus dihapus. Hasil pemeriksaan kualitas data akan digunakan sebagai dasar proses transformasi pada Tahap 4.\n\n##### E. Value\n\nDataset H&M dimanfaatkan untuk menghasilkan informasi yang mendukung evaluasi aktivitas penjualan dan pengambilan keputusan bisnis.\n\n| Fokus Analisis | Informasi yang Dihasilkan | Manfaat Bisnis |\n|---|---|---|\n| Kelompok produk | Kelompok produk dengan total nilai transaksi terbesar | Menentukan prioritas pengelolaan dan evaluasi produk |\n| Perkembangan transaksi | Jumlah pembelian dan total nilai transaksi setiap bulan | Mengidentifikasi perubahan aktivitas penjualan dari waktu ke waktu |\n| Saluran penjualan | Perbandingan jumlah pembelian dan total nilai transaksi berdasarkan saluran | Mengevaluasi kontribusi saluran penjualan fisik dan daring |\n\nInformasi tersebut diperoleh melalui analisis Spark SQL pada Tahap 6 dan digunakan sebagai dasar penyusunan rekomendasi bisnis berdasarkan pola transaksi historis.","metadata":{}},{"cell_type":"markdown","source":"## 2. Jenis Data dan Jenis Analitik\n\n#### 2.1 Klasifikasi Jenis Data\n\nProyek ini menggunakan tiga sumber data utama dari dataset *H&M Personalized Fashion Recommendations*. Seluruh sumber data termasuk **data terstruktur** karena disimpan dalam format CSV dengan susunan kolom yang konsisten dan dapat diolah menggunakan Spark DataFrame.\n\n| Sumber Data | Jenis Data | Alasan |\n|---|---|---|\n| `transactions_train.csv` | Terstruktur | Memiliki lima kolom yang merepresentasikan tanggal pembelian, identitas pelanggan, identitas produk, harga, dan saluran penjualan. |\n| `articles.csv` | Terstruktur | Memiliki atribut produk yang tersusun dalam kolom, seperti kode produk, nama produk, jenis produk, dan kelompok produk. |\n| `customers.csv` | Terstruktur | Memiliki atribut pelanggan yang tersusun dalam kolom, seperti identitas pelanggan, usia, status keanggotaan, dan frekuensi komunikasi. |\n\nKetiga tabel dapat dihubungkan menggunakan kolom `article_id` dan `customer_id` untuk menghasilkan informasi transaksi yang lebih lengkap. Berkas gambar produk pada direktori `images/` termasuk data tidak terstruktur, tetapi tidak digunakan dalam proses ETL karena analisis difokuskan pada data transaksi berbentuk tabel.\n\n#### 2.2 Jenis Analitik Berdasarkan Pertanyaan Bisnis\n\nKetiga pertanyaan bisnis dalam proyek ini menggunakan pendekatan **analitik deskriptif** karena bertujuan merangkum dan membandingkan aktivitas transaksi historis, bukan memprediksi kejadian masa depan atau menentukan keputusan optimal secara otomatis.\n\n| Pertanyaan Bisnis | Jenis Analitik | Alasan |\n|---|---|---|\n| Kelompok produk apa yang memberikan kontribusi total nilai transaksi terbesar? | Deskriptif | Mengidentifikasi dan membandingkan total nilai transaksi berdasarkan kelompok produk. |\n| Bagaimana perkembangan jumlah pembelian produk dan total nilai transaksi setiap bulan? | Deskriptif | Menggambarkan perubahan aktivitas transaksi berdasarkan periode bulanan. |\n| Bagaimana perbandingan jumlah pembelian produk dan total nilai transaksi antara saluran penjualan fisik dan daring? | Deskriptif | Membandingkan aktivitas transaksi historis berdasarkan kategori saluran penjualan. |\n\nHasil analisis akan diperoleh menggunakan agregasi Spark SQL, seperti `COUNT()`, `SUM()`, dan `GROUP BY`, serta disajikan dalam bentuk tabel dan visualisasi.\n\n#### 2.3 Pemetaan Siklus Hidup Data\n\nProses pengolahan dataset H&M dipetakan ke dalam siklus hidup data mulai dari pengumpulan hingga pemanfaatan hasil analisis.\n\n| Tahap Siklus Hidup Data | Implementasi dalam Proyek |\n|---|---|\n| Pengumpulan data (*Data Collection*) | Mengakses dataset H&M melalui Kaggle dan memilih tiga berkas CSV utama. |\n| Penyimpanan dan pembacaan (*Data Ingestion*) | Membaca data transaksi, produk, dan pelanggan menggunakan Apache Spark. |\n| Persiapan dan pengolahan (*Data Processing*) | Memeriksa kualitas data, menangani nilai tidak valid, membuat kolom turunan, serta menggabungkan tabel. |\n| Penyimpanan hasil (*Data Storage*) | Menyimpan hasil transformasi dalam format Parquet dengan partisi berdasarkan kolom yang relevan. |\n| Analisis data (*Data Analysis*) | Menggunakan Spark DataFrame dan Spark SQL untuk menjawab tiga pertanyaan bisnis. |\n| Penyajian dan pemanfaatan (*Data Visualization and Utilization*) | Menyajikan hasil analisis melalui tabel, grafik, insight, dan rekomendasi bisnis. |\n| Perlindungan dan tata kelola (*Data Governance*) | Menerapkan perlindungan terhadap atribut pelanggan serta menetapkan aturan akses, penyimpanan, dan pengelolaan data. |\n\nPenerapan siklus hidup data tersebut bertujuan memastikan bahwa data tidak hanya berhasil diproses, tetapi juga memiliki kualitas yang memadai, tersimpan secara efisien, dan dapat dimanfaatkan untuk mendukung pengambilan keputusan.","metadata":{}},{"cell_type":"markdown","source":"## 3. Extract\n\nTahap *Extract* dilakukan untuk membaca tiga berkas utama dataset *H&M Personalized Fashion Recommendations*, yaitu `transactions_train.csv`, `articles.csv`, dan `customers.csv`, menggunakan Apache Spark melalui PySpark. Ketiga berkas diakses secara langsung dari direktori input Kaggle Notebook dan dimuat sebagai Spark DataFrame.\n\nPada tahap ini, dilakukan pemeriksaan struktur kolom, jumlah baris, ukuran berkas, rentang tanggal transaksi, serta contoh data untuk memastikan bahwa seluruh sumber data dapat dibaca dengan baik sebelum memasuki proses transformasi.","metadata":{}},{"cell_type":"code","source":"# Instalasi PySpark\n%pip -q install pyspark","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T07:59:48.897494Z","iopub.execute_input":"2026-10-14T07:59:48.89787Z","iopub.status.idle":"2026-10-14T07:59:55.07203Z","shell.execute_reply.started":"2026-10-14T07:59:48.897833Z","shell.execute_reply":"2026-10-14T07:59:55.070418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pyspark.sql import SparkSession\nfrom pyspark.sql import functions as F\nfrom pyspark.sql.types import (\n    StructType,\n    StructField,\n    StringType,\n    DoubleType,\n    IntegerType\n)\nfrom pathlib import Path\n\nspark = (\n    SparkSession.builder\n    .appName(\"UTS_Big_Data_HM\")\n    .config(\"spark.sql.shuffle.partitions\", \"8\")\n    .getOrCreate()\n)\n\nprint(\"Apache Spark berhasil dijalankan.\")\nprint(\"Versi Apache Spark:\", spark.version)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T07:59:55.075007Z","iopub.execute_input":"2026-10-14T07:59:55.075874Z","iopub.status.idle":"2026-10-14T08:00:10.324813Z","shell.execute_reply.started":"2026-10-14T07:59:55.075838Z","shell.execute_reply":"2026-10-14T08:00:10.323663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 3.1 Identifikasi Berkas Sumber Data\n\nDataset diakses melalui fitur *Add Input* pada Kaggle Notebook. Pemeriksaan direktori dilakukan untuk mengidentifikasi lokasi dan ukuran tiga berkas CSV yang digunakan. Ukuran berkas dihitung berdasarkan metadata file sebelum proses pembacaan menggunakan Spark.","metadata":{}},{"cell_type":"code","source":"input_dir = Path(\"/kaggle/input\")\n\nnama_file = [\n    \"transactions_train.csv\",\n    \"articles.csv\",\n    \"customers.csv\"\n]\n\nfile_paths = {}\n\nfor nama in nama_file:\n    hasil = list(input_dir.rglob(nama))\n\n    if not hasil:\n        raise FileNotFoundError(f\"File {nama} tidak ditemukan.\")\n\n    file_paths[nama] = hasil[0]\n\ntotal_ukuran = 0\n\nfor nama, lokasi in file_paths.items():\n    ukuran = lokasi.stat().st_size\n    total_ukuran += ukuran\n\n    print(f\"File   : {nama}\")\n    print(f\"Lokasi : {lokasi}\")\n    print(f\"Ukuran : {ukuran / (1024**2):,.2f} MB\")\n    print(\"-\" * 50)\n\nprint(f\"Total ukuran: {total_ukuran / (1024**2):,.2f} MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:00:10.326157Z","iopub.execute_input":"2026-10-14T08:00:10.326758Z","iopub.status.idle":"2026-10-14T08:09:45.63966Z","shell.execute_reply.started":"2026-10-14T08:00:10.326728Z","shell.execute_reply":"2026-10-14T08:09:45.638153Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 3.2 Pembacaan Data Menggunakan Spark DataFrame\n\nKetiga berkas CSV dibaca menggunakan PySpark. Tabel `transactions_train.csv` menggunakan schema eksplisit untuk menetapkan tipe data setiap kolom, sedangkan tabel `articles.csv` dan `customers.csv` dibaca menggunakan tipe string pada tahap awal.\n\nSchema eksplisit digunakan agar tipe data konsisten tanpa proses inferensi dari isi berkas. Kolom `customer_id` dan `article_id` dipertahankan sebagai string karena merupakan kode identitas, sedangkan `price` dibaca sebagai numerik dan `sales_channel_id` sebagai bilangan bulat. Kolom tanggal `t_dat` sementara dibaca sebagai string dan akan dikonversi pada Tahap 4.","metadata":{}},{"cell_type":"code","source":"schema_transaksi = StructType([\n    StructField(\"t_dat\", StringType(), True),\n    StructField(\"customer_id\", StringType(), True),\n    StructField(\"article_id\", StringType(), True),\n    StructField(\"price\", DoubleType(), True),\n    StructField(\"sales_channel_id\", IntegerType(), True)\n])\n\ntransactions_df = (\n    spark.read\n    .option(\"header\", True)\n    .schema(schema_transaksi)\n    .csv(str(file_paths[\"transactions_train.csv\"]))\n)\n\narticles_df = (\n    spark.read\n    .option(\"header\", True)\n    .option(\"inferSchema\", False)\n    .csv(str(file_paths[\"articles.csv\"]))\n)\n\ncustomers_df = (\n    spark.read\n    .option(\"header\", True)\n    .option(\"inferSchema\", False)\n    .csv(str(file_paths[\"customers.csv\"]))\n)\n\nprint(\"Ketiga Spark DataFrame berhasil dibuat.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:09:45.642091Z","iopub.execute_input":"2026-10-14T08:09:45.642883Z","iopub.status.idle":"2026-10-14T08:09:52.161241Z","shell.execute_reply.started":"2026-10-14T08:09:45.642791Z","shell.execute_reply":"2026-10-14T08:09:52.160578Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 3.3 Pemeriksaan Schema, Jumlah Baris, dan Contoh Data\n\nPemeriksaan awal dilakukan dengan menampilkan struktur kolom, menghitung jumlah baris, dan melihat contoh data dari masing-masing tabel. Hasil pemeriksaan digunakan untuk memastikan kesesuaian struktur sumber data dan melengkapi karakteristik Volume pada Tahap 1.","metadata":{}},{"cell_type":"code","source":"print(\"=== SCHEMA TRANSAKSI ===\")\ntransactions_df.printSchema()\n\nprint(\"=== SCHEMA PRODUK ===\")\narticles_df.printSchema()\n\nprint(\"=== SCHEMA PELANGGAN ===\")\ncustomers_df.printSchema()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:09:52.162081Z","iopub.execute_input":"2026-10-14T08:09:52.162319Z","iopub.status.idle":"2026-10-14T08:09:52.180179Z","shell.execute_reply.started":"2026-10-14T08:09:52.162297Z","shell.execute_reply":"2026-10-14T08:09:52.179521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"jumlah_transaksi = transactions_df.count()\njumlah_produk = articles_df.count()\njumlah_pelanggan = customers_df.count()\n\nprint(f\"Jumlah transaksi : {jumlah_transaksi:,}\")\nprint(f\"Jumlah produk    : {jumlah_produk:,}\")\nprint(f\"Jumlah pelanggan : {jumlah_pelanggan:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:09:52.181821Z","iopub.execute_input":"2026-10-14T08:09:52.182407Z","iopub.status.idle":"2026-10-14T08:10:11.648176Z","shell.execute_reply.started":"2026-10-14T08:09:52.182372Z","shell.execute_reply":"2026-10-14T08:10:11.645981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== CONTOH TRANSAKSI ===\")\ntransactions_df.select(\n    \"t_dat\",\n    F.sha2(\"customer_id\", 256).alias(\"customer_id_hash\"),\n    \"article_id\",\n    \"price\",\n    \"sales_channel_id\"\n).show(5, truncate=20)\n\nprint(\"=== CONTOH PRODUK ===\")\narticles_df.select(\n    \"article_id\",\n    \"prod_name\",\n    \"product_group_name\"\n).show(5, truncate=False)\n\nprint(\"=== CONTOH PELANGGAN ===\")\ncustomers_df.select(\n    F.sha2(\"customer_id\", 256).alias(\"customer_id_hash\"),\n    \"age\",\n    \"club_member_status\"\n).show(5, truncate=20)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:10:11.653937Z","iopub.execute_input":"2026-10-14T08:10:11.654895Z","iopub.status.idle":"2026-10-14T08:10:12.489151Z","shell.execute_reply.started":"2026-10-14T08:10:11.654849Z","shell.execute_reply":"2026-10-14T08:10:12.487572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== RENTANG TANGGAL TRANSAKSI ===\")\n\ntransactions_df.agg(\n    F.min(\"t_dat\").alias(\"tanggal_awal\"),\n    F.max(\"t_dat\").alias(\"tanggal_akhir\")\n).show()\n\nprint(\"=== RINGKASAN TRANSAKSI HARIAN ===\")\n\ntransaksi_harian = (\n    transactions_df\n    .groupBy(\"t_dat\")\n    .agg(F.count(\"*\").alias(\"jumlah_pembelian\"))\n)\n\ntransaksi_harian.agg(\n    F.count(\"*\").alias(\"jumlah_hari\"),\n    F.round(F.avg(\"jumlah_pembelian\"), 2).alias(\"rata_rata_harian\"),\n    F.max(\"jumlah_pembelian\").alias(\"tertinggi_per_hari\")\n).show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:10:12.491043Z","iopub.execute_input":"2026-10-14T08:10:12.491359Z","iopub.status.idle":"2026-10-14T08:11:03.786229Z","shell.execute_reply.started":"2026-10-14T08:10:12.491334Z","shell.execute_reply":"2026-10-14T08:11:03.785291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== RINGKASAN DATASET H&M ===\")\n\nprint(\"\\n1. JUMLAH BARIS\")\nprint(f\"Transaksi : {jumlah_transaksi:,}\")\nprint(f\"Produk    : {jumlah_produk:,}\")\nprint(f\"Pelanggan : {jumlah_pelanggan:,}\")\nprint(f\"Total     : {jumlah_transaksi + jumlah_produk + jumlah_pelanggan:,}\")\n\nprint(\"\\n2. UKURAN BERKAS\")\nfor nama, lokasi in file_paths.items():\n    print(f\"{nama}: {lokasi.stat().st_size / (1024**2):,.2f} MB\")\n\nprint(f\"Total: {total_ukuran / (1024**2):,.2f} MB\")\n\nprint(\"\\n3. PERIODE TRANSAKSI\")\ntransactions_df.agg(\n    F.min(\"t_dat\").alias(\"tanggal_awal\"),\n    F.max(\"t_dat\").alias(\"tanggal_akhir\")\n).show()\n\nprint(\"\\n4. AKTIVITAS HARIAN\")\ntransaksi_harian.agg(\n    F.count(\"*\").alias(\"jumlah_hari\"),\n    F.round(F.avg(\"jumlah_pembelian\"), 2).alias(\"rata_rata_harian\"),\n    F.max(\"jumlah_pembelian\").alias(\"tertinggi_per_hari\")\n).show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:11:03.787587Z","iopub.execute_input":"2026-10-14T08:11:03.788348Z","iopub.status.idle":"2026-10-14T08:11:52.694613Z","shell.execute_reply.started":"2026-10-14T08:11:03.788298Z","shell.execute_reply":"2026-10-14T08:11:52.693667Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 3.4 Ringkasan Hasil Extract\n\nProses *Extract* berhasil membaca tiga berkas utama dataset *H&M Personalized Fashion Recommendations* menggunakan Apache Spark, yaitu `transactions_train.csv`, `articles.csv`, dan `customers.csv`. Berdasarkan hasil pemeriksaan, tabel transaksi memiliki **31.788.324 baris**, tabel produk memiliki **105.542 baris**, dan tabel pelanggan memiliki **1.371.980 baris**, dengan total keseluruhan sebanyak **33.265.846 baris** dan ukuran berkas sebesar **3.558,41 MB**.\n\nData transaksi mencakup periode **20 September 2018 hingga 22 September 2020**, dengan aktivitas pembelian yang tercatat selama **734 hari**. Rata-rata jumlah catatan pembelian mencapai **43.308,34 per hari**, sedangkan jumlah pembelian harian tertinggi mencapai **198.622 catatan**.\n\nPemeriksaan schema menunjukkan bahwa ketiga tabel memiliki atribut yang mendukung analisis transaksi, meliputi identitas pelanggan dan produk, harga, kategori saluran penjualan, serta tanggal pembelian. Schema eksplisit diterapkan pada tabel transaksi untuk memastikan konsistensi tipe data tanpa proses inferensi otomatis. Kolom identitas tetap dipertahankan sebagai string, sedangkan kolom tanggal akan dikonversi menjadi tipe data tanggal pada tahap transformasi.\n\nHasil Extract menunjukkan bahwa dataset memenuhi persyaratan volume data UTS dan dapat digunakan untuk proses pembersihan, pembentukan kolom turunan, serta penggabungan antartabel pada tahap berikutnya.","metadata":{}},{"cell_type":"markdown","source":"## 4. Transform\n\nTahap *Transform* dilakukan untuk menyiapkan data transaksi, produk, dan pelanggan sebelum dianalisis menggunakan Spark SQL. Proses ini mencakup pembatasan periode transaksi, pemeriksaan dan penanganan kualitas data, penyesuaian tipe data, pembentukan kolom turunan, perlindungan informasi pelanggan, serta penggabungan antartabel.\n\n#### 4.1 Pembatasan Periode Transaksi\n\nDataset H&M memiliki 31.788.324 catatan pembelian dengan ukuran berkas transaksi sebesar 3.326,42 MB. Untuk menyesuaikan kebutuhan pemrosesan dengan sumber daya Kaggle Notebook, analisis dibatasi pada periode **1 Januari 2020 hingga 30 Juni 2020**.\n\nPemilihan periode enam bulan ini bertujuan mengurangi jumlah data yang diproses pada tahap transformasi, penggabungan tabel, dan analisis. Periode tersebut mencakup beberapa bulan pengamatan sehingga perkembangan transaksi bulanan tetap dapat dianalisis.\n\nJumlah baris sebelum dan sesudah pembatasan periode diperiksa menggunakan Spark DataFrame untuk memastikan bahwa data yang digunakan tetap memenuhi ketentuan minimal satu juta baris pada tabel utama.","metadata":{}},{"cell_type":"code","source":"# Periode analisis yang dipilih\ntanggal_mulai = \"2020-01-01\"\ntanggal_akhir = \"2020-06-30\"\n\n# Jumlah baris sebelum pembatasan\njumlah_sebelum = transactions_df.count()\n\n# Filter transaksi berdasarkan periode\ntransactions_period_df = transactions_df.filter(\n    (F.col(\"t_dat\") >= tanggal_mulai) &\n    (F.col(\"t_dat\") <= tanggal_akhir)\n)\n\n# Jumlah baris setelah pembatasan\njumlah_sesudah = transactions_period_df.count()\n\n# Ringkasan hasil\nprint(\"=== PEMBATASAN PERIODE TRANSAKSI ===\")\nprint(f\"Periode analisis       : {tanggal_mulai} s.d. {tanggal_akhir}\")\nprint(f\"Jumlah sebelum filter  : {jumlah_sebelum:,} baris\")\nprint(f\"Jumlah setelah filter  : {jumlah_sesudah:,} baris\")\nprint(f\"Jumlah tidak digunakan : {jumlah_sebelum - jumlah_sesudah:,} baris\")\nprint(f\"Persentase dipertahankan: {jumlah_sesudah / jumlah_sebelum * 100:.2f}%\")\n\n# Pemeriksaan persyaratan UTS\nif jumlah_sesudah >= 1_000_000:\n    print(\"\\nMEMENUHI SYARAT: minimal 1 juta baris.\")\nelse:\n    print(\"\\nBELUM MEMENUHI SYARAT: periode perlu diperluas.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:11:52.696227Z","iopub.execute_input":"2026-10-14T08:11:52.697053Z","iopub.status.idle":"2026-10-14T08:12:18.995755Z","shell.execute_reply.started":"2026-10-14T08:11:52.697006Z","shell.execute_reply":"2026-10-14T08:12:18.994025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== VALIDASI PERIODE HASIL FILTER ===\")\n\ntransactions_period_df.agg(\n    F.min(\"t_dat\").alias(\"tanggal_pertama\"),\n    F.max(\"t_dat\").alias(\"tanggal_terakhir\"),\n    F.count(\"*\").alias(\"jumlah_baris\")\n).show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:12:18.998332Z","iopub.execute_input":"2026-10-14T08:12:18.998759Z","iopub.status.idle":"2026-10-14T08:12:38.707111Z","shell.execute_reply.started":"2026-10-14T08:12:18.99872Z","shell.execute_reply":"2026-10-14T08:12:38.705051Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Hasil Pembatasan Periode\n\nPembatasan periode dilakukan terhadap tabel transaksi H&M dari 31.788.324 catatan pembelian pada periode 20 September 2018 hingga 22 September 2020 menjadi **[jumlah baris setelah filter]** catatan pembelian pada periode 1 Januari 2020 hingga 30 Juni 2020.\n\nSebanyak **[jumlah baris yang tidak digunakan]** catatan pembelian berada di luar periode analisis, sehingga data yang dipertahankan mencakup **[persentase]%** dari keseluruhan transaksi awal.\n\nPembatasan ini dilakukan untuk mengurangi beban pemrosesan tanpa mengubah isi transaksi pada periode yang dipilih. Data hasil filter tetap memenuhi persyaratan minimal satu juta baris dan selanjutnya digunakan untuk proses pemeriksaan kualitas serta transformasi data.","metadata":{}},{"cell_type":"markdown","source":"#### 4.2 Pemeriksaan dan Penanganan Kualitas Data\n\n##### 4.2.1 Pemeriksaan Nilai Kosong\n\nPemeriksaan nilai kosong dilakukan pada tabel transaksi, produk, dan pelanggan sebelum proses pembersihan data.","metadata":{}},{"cell_type":"code","source":"from pyspark.sql import functions as F\n\ndef cek_missing_rapi(df, nama_tabel):\n    total = df.count()\n\n    hasil = df.select([\n        F.sum(\n            F.when(\n                F.col(c).isNull() |\n                (F.trim(F.col(c).cast(\"string\")) == \"\"),\n                1\n            ).otherwise(0)\n        ).alias(c)\n        for c in df.columns\n    ]).collect()[0].asDict()\n\n    print(f\"\\n=== {nama_tabel} ===\")\n    print(f\"Total baris: {total:,}\")\n    print(f\"{'Kolom':<32} {'Jumlah Missing':>16} {'Persentase':>12}\")\n    print(\"-\" * 64)\n\n    for kolom, jumlah in sorted(\n        hasil.items(), key=lambda x: x[1], reverse=True\n    ):\n        persen = jumlah / total * 100 if total else 0\n\n        print(\n            f\"{kolom:<32} \"\n            f\"{jumlah:>16,} \"\n            f\"{persen:>11.2f}%\"\n        )\n\ncek_missing_rapi(transactions_period_df, \"TRANSAKSI\")\ncek_missing_rapi(articles_df, \"PRODUK\")\ncek_missing_rapi(customers_df, \"PELANGGAN\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:12:38.708654Z","iopub.execute_input":"2026-10-14T08:12:38.709056Z","iopub.status.idle":"2026-10-14T08:13:29.667663Z","shell.execute_reply.started":"2026-10-14T08:12:38.709019Z","shell.execute_reply":"2026-10-14T08:13:29.666844Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### 4.2.2 Penanganan Nilai Kosong\n\nBerdasarkan hasil pemeriksaan, tabel transaksi tidak memiliki nilai kosong. Sementara itu, nilai kosong ditemukan pada kolom `detail_desc` di tabel produk serta lima kolom pada tabel pelanggan. Penanganan dilakukan berdasarkan jenis informasi pada setiap kolom dan kebutuhan analisis, dengan tetap mempertahankan data yang masih dapat digunakan.\n\n| Tabel | Kolom | Jumlah Missing | Penanganan | Alasan |\n|---|---|---:|---|---|\n| Transaksi | Seluruh kolom | 0 | Tidak dilakukan perubahan | Seluruh atribut transaksi telah terisi sehingga tidak diperlukan penanganan nilai kosong. |\n| Produk | `detail_desc` | 416 (0,39%) | Diisi `Tidak tersedia` | Deskripsi yang tidak tercatat diberi penanda khusus agar dapat dibedakan dari deskripsi yang tersedia, tanpa menghilangkan informasi produk lainnya. |\n| Pelanggan | `Active` | 907.576 (66,15%) | Nilai kosong dipertahankan | Nilai kosong tidak dapat langsung dikategorikan sebagai pelanggan aktif maupun tidak aktif karena tidak tersedia informasi yang mendukung penentuan status tersebut. |\n| Pelanggan | `FN` | 895.050 (65,24%) | Nilai kosong dipertahankan | Informasi yang tidak tercatat tidak dapat langsung dianggap sebagai kondisi tertentu karena berpotensi menghasilkan interpretasi yang tidak sesuai dengan data sebenarnya. |\n| Pelanggan | `fashion_news_frequency` | 16.009 (1,17%) | Diisi `Unknown` | Penggunaan kategori `Unknown` memungkinkan data dengan informasi frekuensi yang tidak tersedia tetap dipertahankan tanpa mengubahnya menjadi kategori frekuensi tertentu. |\n| Pelanggan | `age` | 15.861 (1,16%) | Nilai kosong dipertahankan | Pengisian menggunakan nilai rata-rata atau median dapat menghasilkan usia yang tidak sesuai dengan kondisi pelanggan sebenarnya. Selain itu, usia tidak digunakan dalam analisis utama. |\n| Pelanggan | `club_member_status` | 6.062 (0,44%) | Diisi `Unknown` | Status keanggotaan yang tidak tercatat diberi kategori tersendiri agar tidak disamakan dengan status keanggotaan yang sudah diketahui. |\n\nPenanganan nilai kosong dilakukan menggunakan fungsi `fillna()` pada kolom `detail_desc`, `fashion_news_frequency`, dan `club_member_status`. Sementara itu, nilai kosong pada kolom `Active`, `FN`, dan `age` dipertahankan karena tidak tersedia dasar yang cukup untuk menentukan nilai penggantinya. Ketiga kolom tersebut juga tidak diperlukan untuk menjawab pertanyaan bisnis yang telah ditetapkan.\n\nProses ini tidak melibatkan penghapusan baris sehingga jumlah data tetap **7.593.169 transaksi, 105.542 produk, dan 1.371.980 pelanggan**. Dengan demikian, seluruh catatan yang tersedia tetap dapat digunakan pada tahap transformasi berikutnya.","metadata":{}},{"cell_type":"markdown","source":"##### 4.2.3 Pemeriksaan Duplikat\n\nPemeriksaan duplikat dilakukan pada tabel transaksi, produk, dan pelanggan. Pada tabel transaksi, pemeriksaan dilakukan terhadap seluruh kolom untuk mengetahui jumlah catatan yang identik. Sementara itu, pada tabel produk dan pelanggan, pemeriksaan difokuskan pada `article_id` dan `customer_id` karena kedua kolom tersebut merupakan identitas unik yang digunakan dalam proses penggabungan data.","metadata":{}},{"cell_type":"code","source":"# Data hasil penanganan missing value\ntransactions_clean_df = transactions_period_df\n\narticles_clean_df = articles_df.fillna({\n    \"detail_desc\": \"Tidak tersedia\"\n})\n\ncustomers_clean_df = customers_df.fillna({\n    \"fashion_news_frequency\": \"Unknown\",\n    \"club_member_status\": \"Unknown\"\n})\n\nprint(\"Data hasil penanganan missing value siap digunakan.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:15:30.242493Z","iopub.execute_input":"2026-10-14T08:15:30.243114Z","iopub.status.idle":"2026-10-14T08:15:30.280715Z","shell.execute_reply.started":"2026-10-14T08:15:30.243083Z","shell.execute_reply":"2026-10-14T08:15:30.279674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pemeriksaan duplikat\ndef cek_duplikat(df, kolom_kunci, nama_tabel):\n    total = df.count()\n\n    # Duplikat berdasarkan seluruh kolom\n    unik_seluruh = df.dropDuplicates().count()\n    duplikat_seluruh = total - unik_seluruh\n\n    # Duplikat berdasarkan kolom kunci\n    unik_kunci = df.select(*kolom_kunci).distinct().count()\n    duplikat_kunci = total - unik_kunci\n\n    return (nama_tabel, total, duplikat_seluruh, duplikat_kunci)\n\n\nhasil_duplikat = [\n    cek_duplikat(\n        transactions_clean_df,\n        [\"t_dat\", \"customer_id\", \"article_id\", \"price\", \"sales_channel_id\"],\n        \"Transaksi\"\n    ),\n    cek_duplikat(\n        articles_clean_df,\n        [\"article_id\"],\n        \"Produk\"\n    ),\n    cek_duplikat(\n        customers_clean_df,\n        [\"customer_id\"],\n        \"Pelanggan\"\n    )\n]\n\nprint(\"=== HASIL PEMERIKSAAN DUPLIKAT ===\")\nprint(\n    f\"{'Tabel':<15} {'Total Baris':>15} \"\n    f\"{'Duplikat Penuh':>18} {'Duplikat Kunci':>18}\"\n)\nprint(\"-\" * 70)\n\nfor nama, total, dup_penuh, dup_kunci in hasil_duplikat:\n    print(\n        f\"{nama:<15} {total:>15,} \"\n        f\"{dup_penuh:>18,} {dup_kunci:>18,}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:15:44.365996Z","iopub.execute_input":"2026-10-14T08:15:44.36703Z","iopub.status.idle":"2026-10-14T08:17:51.197492Z","shell.execute_reply.started":"2026-10-14T08:15:44.366995Z","shell.execute_reply":"2026-10-14T08:17:51.196785Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### 4.2.4 Penanganan Duplikat\n\nBerdasarkan hasil pemeriksaan, ditemukan 721.828 baris duplikat dari 7.593.169 catatan transaksi (9,51%). Sementara itu, tabel produk dan pelanggan tidak memiliki duplikat, baik berdasarkan seluruh kolom maupun berdasarkan identitas unik masing-masing tabel.\n\n| Tabel | Jumlah Baris | Duplikat | Penanganan |\n|---|---:|---:|---|\n| Transaksi | 7.593.169 | 721.828 | Dipertahankan |\n| Produk | 105.542 | 0 | Tidak dilakukan perubahan |\n| Pelanggan | 1.371.980 | 0 | Tidak dilakukan perubahan |\n\n**Alasan penanganan:**\n\nPada tabel transaksi, pemeriksaan duplikat dilakukan berdasarkan lima kolom, yaitu `t_dat`, `customer_id`, `article_id`, `price`, dan `sales_channel_id`. Meskipun terdapat baris dengan nilai yang sama pada seluruh kolom tersebut, dataset tidak menyediakan `transaction_id` atau `order_id` yang dapat digunakan untuk membedakan setiap pembelian.\n\nKolom `customer_id` merupakan identitas pelanggan, sedangkan `article_id` merupakan identitas produk. Keduanya tidak dapat digunakan sebagai identitas unik transaksi karena seorang pelanggan dapat membeli produk yang sama lebih dari satu kali, termasuk pada tanggal yang sama. Dengan demikian, baris yang identik belum dapat dipastikan sebagai kesalahan pencatatan maupun pembelian yang berbeda.\n\nOleh karena itu, sebanyak 721.828 baris duplikat tetap dipertahankan. Penghapusan tanpa bukti yang cukup berisiko mengurangi jumlah pembelian dan total harga yang dihitung dalam analisis. Namun, keputusan ini juga memiliki keterbatasan karena keabsahan setiap catatan yang berulang tidak dapat diverifikasi menggunakan atribut yang tersedia.\n\nPada tabel produk, setiap `article_id` hanya muncul satu kali. Begitu pula pada tabel pelanggan, setiap `customer_id` hanya muncul satu kali. Dengan demikian, kedua tabel tidak memerlukan penghapusan duplikat dan dapat digunakan sebagai tabel pendukung dalam proses penggabungan data.\n\nSetelah penanganan, jumlah data tetap sebesar **7.593.169 transaksi, 105.542 produk, dan 1.371.980 pelanggan**.","metadata":{}},{"cell_type":"code","source":"# Mempertahankan seluruh baris berdasarkan hasil pemeriksaan\ntransactions_dedup_df = transactions_clean_df\narticles_dedup_df = articles_clean_df\ncustomers_dedup_df = customers_clean_df\n\n# Jumlah baris sebelum dan sesudah penanganan\ndata_duplikat = [\n    (\"Transaksi\", transactions_clean_df, transactions_dedup_df),\n    (\"Produk\", articles_clean_df, articles_dedup_df),\n    (\"Pelanggan\", customers_clean_df, customers_dedup_df)\n]\n\nprint(\"=== HASIL PENANGANAN DUPLIKAT ===\")\nprint(f\"{'Tabel':<15} {'Sebelum':>15} {'Sesudah':>15} {'Selisih':>12}\")\nprint(\"-\" * 62)\n\nfor nama, df_sebelum, df_sesudah in data_duplikat:\n    sebelum = df_sebelum.count()\n    sesudah = df_sesudah.count()\n\n    print(\n        f\"{nama:<15} \"\n        f\"{sebelum:>15,} \"\n        f\"{sesudah:>15,} \"\n        f\"{sesudah - sebelum:>12,}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:29:12.920649Z","iopub.execute_input":"2026-10-14T08:29:12.921444Z","iopub.status.idle":"2026-10-14T08:30:01.567511Z","shell.execute_reply.started":"2026-10-14T08:29:12.921412Z","shell.execute_reply":"2026-10-14T08:30:01.56388Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### 4.2.5 Pemeriksaan dan Perbaikan Tipe Data\n\nPemeriksaan tipe data dilakukan pada tabel transaksi, produk, dan pelanggan untuk memastikan setiap kolom memiliki tipe data yang sesuai. Pada tahap Extract, kolom `t_dat` masih dibaca sebagai string, sedangkan `price` dan `sales_channel_id` telah menggunakan tipe numerik melalui schema eksplisit.\n\nPerbaikan dilakukan dengan mengubah `t_dat` menjadi tipe `date` dan `age` menjadi tipe `integer`. Sementara itu, `customer_id` dan `article_id` tetap menggunakan tipe string karena berfungsi sebagai identitas, bukan nilai untuk perhitungan matematis. Jumlah baris sebelum dan sesudah konversi diperiksa untuk memastikan tidak ada data yang hilang.","metadata":{}},{"cell_type":"code","source":"print(\"=== TIPE DATA TRANSAKSI ===\")\ntransactions_dedup_df.printSchema()\n\nprint(\"\\n=== TIPE DATA PRODUK ===\")\narticles_dedup_df.printSchema()\n\nprint(\"\\n=== TIPE DATA PELANGGAN ===\")\ncustomers_dedup_df.printSchema()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:31:33.96463Z","iopub.execute_input":"2026-10-14T08:31:33.965121Z","iopub.status.idle":"2026-10-14T08:31:33.977548Z","shell.execute_reply.started":"2026-10-14T08:31:33.965094Z","shell.execute_reply":"2026-10-14T08:31:33.976656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pyspark.sql import functions as F\n\n# Konversi tanggal transaksi\ntransactions_typed_df = transactions_dedup_df.withColumn(\n    \"t_dat\",\n    F.to_date(F.col(\"t_dat\"), \"yyyy-MM-dd\")\n)\n\n# Produk tidak memerlukan konversi untuk analisis utama\narticles_typed_df = articles_dedup_df\n\n# Konversi usia pelanggan\ncustomers_typed_df = customers_dedup_df.withColumn(\n    \"age\",\n    F.col(\"age\").cast(\"int\")\n)\n\nprint(\"=== HASIL KONVERSI TIPE DATA ===\")\n\nprint(\"\\nTransaksi:\")\ntransactions_typed_df.select(\n    \"t_dat\", \"price\", \"sales_channel_id\"\n).printSchema()\n\nprint(\"\\nPelanggan:\")\ncustomers_typed_df.select(\"age\").printSchema()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:31:51.10834Z","iopub.execute_input":"2026-10-14T08:31:51.108727Z","iopub.status.idle":"2026-10-14T08:31:51.261992Z","shell.execute_reply.started":"2026-10-14T08:31:51.1087Z","shell.execute_reply":"2026-10-14T08:31:51.260044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pemeriksaan kegagalan konversi\ngagal_tanggal = transactions_typed_df.filter(\n    F.col(\"t_dat\").isNull()\n).count()\n\ngagal_usia = customers_typed_df.filter(\n    F.col(\"age\").isNull() &\n    F.col(\"customer_id\").isNotNull()\n).count()\n\n# Perbandingan jumlah baris\nprint(\"=== VALIDASI KONVERSI TIPE DATA ===\")\nprint(f\"Tanggal NULL setelah konversi : {gagal_tanggal:,}\")\nprint(f\"Usia NULL setelah konversi    : {gagal_usia:,}\")\nprint(\"\\n=== JUMLAH BARIS ===\")\nprint(f\"{'Tabel':<15} {'Sebelum':>15} {'Sesudah':>15}\")\nprint(\"-\" * 47)\n\nfor nama, sebelum, sesudah in [\n    (\"Transaksi\", transactions_dedup_df, transactions_typed_df),\n    (\"Produk\", articles_dedup_df, articles_typed_df),\n    (\"Pelanggan\", customers_dedup_df, customers_typed_df)\n]:\n    print(\n        f\"{nama:<15} \"\n        f\"{sebelum.count():>15,} \"\n        f\"{sesudah.count():>15,}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:32:10.936394Z","iopub.execute_input":"2026-10-14T08:32:10.936771Z","iopub.status.idle":"2026-10-14T08:33:16.63458Z","shell.execute_reply.started":"2026-10-14T08:32:10.936739Z","shell.execute_reply":"2026-10-14T08:33:16.632884Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### 4.2.6 Pemeriksaan Nilai Tidak Valid\n\nPemeriksaan dilakukan pada kolom `price`, `age`, dan `sales_channel_id`. Pada kolom `price`, diperiksa jumlah harga negatif dan nol. Pada kolom `age`, diperiksa usia di bawah 0 tahun dan di atas 100 tahun sebagai batas awal untuk mengidentifikasi nilai yang perlu ditinjau.\n\nSelain itu, nilai unik pada `sales_channel_id` diperiksa untuk mengetahui kode saluran penjualan yang terdapat dalam dataset. Hasil pemeriksaan digunakan sebagai dasar untuk menentukan penanganan data yang tidak valid.","metadata":{}},{"cell_type":"code","source":"print(\"=== PEMERIKSAAN HARGA ===\")\n\ntransactions_typed_df.agg(\n    F.count(\"*\").alias(\"total_transaksi\"),\n    F.sum(F.when(F.col(\"price\") < 0, 1).otherwise(0)).alias(\"harga_negatif\"),\n    F.sum(F.when(F.col(\"price\") == 0, 1).otherwise(0)).alias(\"harga_nol\"),\n    F.min(\"price\").alias(\"harga_minimum\"),\n    F.max(\"price\").alias(\"harga_maksimum\")\n).show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:38:26.866868Z","iopub.execute_input":"2026-10-14T08:38:26.868513Z","iopub.status.idle":"2026-10-14T08:38:47.454316Z","shell.execute_reply.started":"2026-10-14T08:38:26.868474Z","shell.execute_reply":"2026-10-14T08:38:47.452407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== PEMERIKSAAN USIA ===\")\n\ncustomers_typed_df.agg(\n    F.sum(F.when(F.col(\"age\") < 0, 1).otherwise(0)).alias(\"usia_negatif\"),\n    F.sum(F.when(F.col(\"age\") > 100, 1).otherwise(0)).alias(\"usia_di_atas_100\"),\n    F.min(\"age\").alias(\"usia_minimum\"),\n    F.max(\"age\").alias(\"usia_maksimum\")\n).show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:38:59.542209Z","iopub.execute_input":"2026-10-14T08:38:59.542548Z","iopub.status.idle":"2026-10-14T08:39:01.803508Z","shell.execute_reply.started":"2026-10-14T08:38:59.542523Z","shell.execute_reply":"2026-10-14T08:39:01.802505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== DISTRIBUSI SALURAN PENJUALAN ===\")\n\ntransactions_typed_df.groupBy(\"sales_channel_id\") \\\n    .agg(F.count(\"*\").alias(\"jumlah_transaksi\")) \\\n    .orderBy(\"sales_channel_id\") \\\n    .show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:39:15.143532Z","iopub.execute_input":"2026-10-14T08:39:15.143852Z","iopub.status.idle":"2026-10-14T08:39:36.78136Z","shell.execute_reply.started":"2026-10-14T08:39:15.143827Z","shell.execute_reply":"2026-10-14T08:39:36.78039Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Pemeriksaan Nilai Tidak Valid**\n\nBerdasarkan hasil pemeriksaan terhadap 7.593.169 catatan transaksi, tidak ditemukan harga negatif maupun harga bernilai nol. Nilai harga minimum sebesar 0,00003390 dan maksimum sebesar 0,50677966. Harga dalam dataset H&M menggunakan nilai yang telah dinormalisasi sehingga tidak menunjukkan nominal dalam mata uang tertentu.\n\nPada tabel pelanggan, usia yang tercatat berada pada rentang 16 hingga 99 tahun. Tidak ditemukan usia negatif maupun usia di atas 100 tahun. Nilai usia yang kosong tetap dipertahankan sesuai keputusan pada tahap penanganan *missing value*.\n\nPemeriksaan saluran penjualan menunjukkan dua kode, yaitu `1` sebanyak 1.778.154 catatan pembelian (23,42%) dan `2` sebanyak 5.815.015 catatan pembelian (76,58%). Tidak ditemukan kode lain dalam periode analisis.\n\nBerdasarkan pemeriksaan tersebut, tidak diperlukan penghapusan baris pada tahap ini. Jumlah data tetap sebesar **7.593.169 transaksi, 105.542 produk, dan 1.371.980 pelanggan**.","metadata":{}},{"cell_type":"markdown","source":"### 4.3 Pembentukan Kolom Turunan (*Feature Engineering*)\n\nPembentukan kolom turunan dilakukan pada tabel transaksi dengan memanfaatkan kolom `t_dat` yang telah dikonversi menjadi tipe `date`. Tiga kolom baru yang dibuat adalah `tahun`, `bulan`, dan `tahun_bulan`.\n\nKolom `tahun` digunakan untuk mengidentifikasi tahun transaksi, kolom `bulan` untuk mengidentifikasi bulan transaksi, sedangkan kolom `tahun_bulan` digunakan untuk mengelompokkan transaksi berdasarkan periode bulanan.\n\nPembentukan kolom tersebut mendukung analisis perkembangan jumlah pembelian dan total nilai `price` setiap bulan selama Januari hingga Juni 2020 tanpa mengubah jumlah baris transaksi.","metadata":{}},{"cell_type":"code","source":"# Membuat tiga kolom turunan dari tanggal transaksi\ntransactions_feature_df = (\n    transactions_typed_df\n    .withColumn(\"tahun\", F.year(\"t_dat\"))\n    .withColumn(\"bulan\", F.month(\"t_dat\"))\n    .withColumn(\"tahun_bulan\", F.date_format(\"t_dat\", \"yyyy-MM\"))\n)\n\n# Menampilkan contoh hasil\ntransactions_feature_df.select(\n    \"t_dat\", \"tahun\", \"bulan\", \"tahun_bulan\"\n).show(10, truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:52:09.526722Z","iopub.execute_input":"2026-10-14T08:52:09.529935Z","iopub.status.idle":"2026-10-14T08:52:24.921835Z","shell.execute_reply.started":"2026-10-14T08:52:09.529809Z","shell.execute_reply":"2026-10-14T08:52:24.920811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== VALIDASI PEMBENTUKAN KOLOM TURUNAN ===\")\n\nprint(\"Jumlah baris sebelum :\", transactions_typed_df.count())\nprint(\"Jumlah baris sesudah :\", transactions_feature_df.count())\n\nprint(\"\\n=== JUMLAH NULL PADA KOLOM BARU ===\")\n\ntransactions_feature_df.select(\n    F.sum(F.col(\"tahun\").isNull().cast(\"int\")).alias(\"tahun_null\"),\n    F.sum(F.col(\"bulan\").isNull().cast(\"int\")).alias(\"bulan_null\"),\n    F.sum(F.col(\"tahun_bulan\").isNull().cast(\"int\")).alias(\"tahun_bulan_null\")\n).show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:52:50.403738Z","iopub.execute_input":"2026-10-14T08:52:50.404121Z","iopub.status.idle":"2026-10-14T08:53:54.851838Z","shell.execute_reply.started":"2026-10-14T08:52:50.404097Z","shell.execute_reply":"2026-10-14T08:53:54.850114Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Validasi Kolom Turunan**\n\nHasil validasi menunjukkan bahwa pembentukan kolom `tahun`, `bulan`, dan `tahun_bulan` berhasil dilakukan tanpa mengubah jumlah data transaksi. Jumlah baris sebelum dan sesudah transformasi tetap sebanyak 7.593.169 baris. Selain itu, tidak ditemukan nilai kosong pada ketiga kolom baru. Dengan demikian, kolom turunan dapat digunakan untuk analisis transaksi berdasarkan periode waktu.","metadata":{}},{"cell_type":"markdown","source":"### 4.4 Penggabungan Tabel (*Join*)\n\nPenggabungan data dilakukan antara tabel transaksi dan tabel produk menggunakan kolom `article_id` sebagai kunci penghubung. Tujuannya adalah menambahkan informasi produk, khususnya `product_group_name`, ke dalam setiap catatan transaksi.\n\nMetode yang digunakan adalah *left join*, dengan tabel transaksi sebagai tabel utama. Metode ini dipilih untuk mempertahankan seluruh catatan transaksi, termasuk apabila terdapat `article_id` yang tidak memiliki pasangan pada tabel produk. Hasil penggabungan akan diperiksa untuk mengetahui jumlah baris dan keberadaan produk yang tidak berhasil dicocokkan.","metadata":{}},{"cell_type":"code","source":"# Menggabungkan transaksi dengan informasi produk\ntransactions_articles_df = transactions_feature_df.join(\n    articles_typed_df.select(\n        \"article_id\",\n        \"prod_name\",\n        \"product_group_name\",\n        \"product_type_name\"\n    ),\n    on=\"article_id\",\n    how=\"left\"\n)\n\n# Memeriksa hasil penggabungan\nprint(\"=== VALIDASI JOIN TRANSAKSI DAN PRODUK ===\")\n\nprint(\"Baris sebelum JOIN :\", transactions_feature_df.count())\nprint(\"Baris sesudah JOIN :\", transactions_articles_df.count())\n\nprint(\n    \"Transaksi tanpa pasangan produk :\",\n    transactions_articles_df.filter(\n        F.col(\"product_group_name\").isNull()\n    ).count()\n)\n\ntransactions_articles_df.select(\n    \"t_dat\",\n    \"article_id\",\n    \"prod_name\",\n    \"product_group_name\",\n    \"price\"\n).show(5, truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T08:55:54.415542Z","iopub.execute_input":"2026-10-14T08:55:54.416253Z","iopub.status.idle":"2026-10-14T08:57:07.098054Z","shell.execute_reply.started":"2026-10-14T08:55:54.416218Z","shell.execute_reply":"2026-10-14T08:57:07.097032Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Penggabungan Transaksi dan Produk**\n\nPenggabungan tabel transaksi dan produk menggunakan metode *left join* menghasilkan 7.593.169 baris, sama dengan jumlah transaksi sebelum penggabungan. Selain itu, tidak ditemukan transaksi yang gagal dicocokkan dengan tabel produk. Hasil ini menunjukkan bahwa seluruh `article_id` pada transaksi periode Januari–Juni 2020 memiliki pasangan pada tabel produk.\n\nMelalui penggabungan ini, setiap catatan transaksi memperoleh informasi nama dan kelompok produk yang diperlukan untuk analisis berdasarkan kategori produk.","metadata":{}},{"cell_type":"markdown","source":"#### Penggabungan Transaksi dengan Data Pelanggan\n\nPenggabungan berikutnya dilakukan antara hasil penggabungan transaksi dan produk dengan tabel pelanggan menggunakan kolom `customer_id`. Informasi pelanggan yang ditambahkan meliputi `age` dan `club_member_status`.\n\nMetode *left join* kembali digunakan agar seluruh catatan transaksi tetap dipertahankan. Kolom `postal_code`, `FN`, dan `Active` tidak disertakan karena tidak diperlukan untuk menjawab pertanyaan bisnis utama. Selain itu, pengurangan atribut yang tidak digunakan membantu membatasi penyertaan informasi pelanggan dalam data hasil transformasi.\n\nHasil penggabungan diperiksa berdasarkan jumlah baris sebelum dan sesudah proses serta jumlah transaksi yang tidak memiliki pasangan pelanggan.","metadata":{}},{"cell_type":"code","source":"# Menggabungkan transaksi-produk dengan data pelanggan\ntransactions_joined_df = transactions_articles_df.join(\n    customers_typed_df.select(\n        \"customer_id\",\n        \"age\",\n        \"club_member_status\"\n    ),\n    on=\"customer_id\",\n    how=\"left\"\n)\n\nprint(\"=== VALIDASI JOIN TRANSAKSI DAN PELANGGAN ===\")\n\nprint(\"Baris sebelum JOIN :\", transactions_articles_df.count())\nprint(\"Baris sesudah JOIN :\", transactions_joined_df.count())\n\nprint(\n    \"Transaksi tanpa pasangan pelanggan :\",\n    transactions_joined_df.filter(\n        F.col(\"club_member_status\").isNull()\n    ).count()\n)\n\ntransactions_joined_df.select(\n    \"t_dat\",\n    \"article_id\",\n    \"product_group_name\",\n    \"age\",\n    \"club_member_status\",\n    \"price\"\n).show(5, truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:00:36.308042Z","iopub.execute_input":"2026-10-14T09:00:36.308565Z","iopub.status.idle":"2026-10-14T09:02:28.015167Z","shell.execute_reply.started":"2026-10-14T09:00:36.308526Z","shell.execute_reply":"2026-10-14T09:02:28.012745Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Penggabungan Transaksi dan Pelanggan**\n\nPenggabungan data transaksi dengan tabel pelanggan menggunakan metode *left join* menghasilkan 7.593.169 baris, sama dengan jumlah transaksi sebelum penggabungan. Seluruh transaksi berhasil dicocokkan dengan data pelanggan sehingga tidak ditemukan transaksi tanpa pasangan.\n\nBeberapa catatan masih memiliki nilai `NULL` pada kolom `age` karena informasi usia memang tidak tersedia pada data sumber. Nilai tersebut tetap dipertahankan sesuai keputusan penanganan *missing value* sebelumnya.\n\nDengan demikian, hasil penggabungan telah memuat informasi transaksi, produk, dan pelanggan tanpa mengubah jumlah catatan transaksi.","metadata":{}},{"cell_type":"markdown","source":"### 4.5 Perlindungan Identitas Pelanggan (*Hashing*)\n\nSetelah penggabungan tabel transaksi, produk, dan pelanggan, dilakukan perlindungan terhadap kolom `customer_id` menggunakan algoritma *hashing* SHA-256.\n\nProses ini menghasilkan kolom `customer_hash` sebagai pengganti `customer_id`. Nilai *hash* digunakan agar transaksi dari pelanggan yang sama tetap dapat dihubungkan tanpa menyertakan pengenal pelanggan asli dalam data hasil transformasi.\n\nKolom `customer_id` kemudian dihapus dari DataFrame hasil transformasi. Penerapan teknik ini merupakan bagian dari perlindungan privasi dalam proses ETL yang akan dibahas lebih lanjut pada Tahap 7.","metadata":{}},{"cell_type":"code","source":"# Mengubah customer_id menjadi hash SHA-256\ntransactions_final_df = (\n    transactions_joined_df\n    .withColumn(\n        \"customer_hash\",\n        F.sha2(F.col(\"customer_id\"), 256)\n    )\n    .drop(\"customer_id\")\n)\n\n# Menampilkan contoh hasil hashing\ntransactions_final_df.select(\n    \"customer_hash\",\n    \"t_dat\",\n    \"article_id\",\n    \"product_group_name\"\n).show(5, truncate=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:07:27.109202Z","iopub.execute_input":"2026-10-14T09:07:27.110427Z","iopub.status.idle":"2026-10-14T09:07:45.972563Z","shell.execute_reply.started":"2026-10-14T09:07:27.110387Z","shell.execute_reply":"2026-10-14T09:07:45.971426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=== VALIDASI HASHING ===\")\n\nprint(\"Baris sebelum :\", transactions_joined_df.count())\nprint(\"Baris sesudah :\", transactions_final_df.count())\n\nprint(\n    \"customer_id masih tersedia :\",\n    \"customer_id\" in transactions_final_df.columns\n)\n\nprint(\n    \"customer_hash tersedia :\",\n    \"customer_hash\" in transactions_final_df.columns\n)\n\nprint(\n    \"Jumlah customer_hash NULL :\",\n    transactions_final_df.filter(\n        F.col(\"customer_hash\").isNull()\n    ).count()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:09:04.034633Z","iopub.execute_input":"2026-10-14T09:09:04.035334Z","iopub.status.idle":"2026-10-14T09:10:34.611817Z","shell.execute_reply.started":"2026-10-14T09:09:04.035303Z","shell.execute_reply":"2026-10-14T09:10:34.610679Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Validasi Hashing**\n\nHasil validasi menunjukkan bahwa penerapan *hashing* SHA-256 berhasil dilakukan pada kolom `customer_id`. Jumlah transaksi sebelum dan sesudah proses tetap sebanyak 7.593.169 baris. Kolom `customer_id` telah dihapus dari DataFrame hasil transformasi dan digantikan oleh kolom `customer_hash`, tanpa ditemukan nilai kosong pada kolom tersebut.\n\nDengan demikian, data hasil transformasi tidak lagi menyertakan pengenal pelanggan asli. Meskipun demikian, nilai *hash* masih memungkinkan pengaitan transaksi milik pelanggan yang sama sehingga perlindungan akses terhadap data tetap diperlukan.","metadata":{}},{"cell_type":"markdown","source":"## 5. Load: Penyimpanan Data Hasil ETL\n\nPada tahap *Load*, data yang telah melalui proses pembersihan, pembentukan kolom turunan, penggabungan tabel, dan perlindungan identitas pelanggan disimpan dalam format Parquet.\n\nFormat Parquet dipilih karena menggunakan penyimpanan berbasis kolom (*columnar storage*) yang mendukung kompresi serta pembacaan kolom tertentu secara efisien pada Apache Spark.\n\nPenyimpanan dilakukan dengan partisi berdasarkan kolom `tahun_bulan` sehingga transaksi Januari hingga Juni 2020 dikelompokkan ke dalam direktori masing-masing bulan. Pemilihan partisi ini sesuai dengan kebutuhan analisis perkembangan transaksi bulanan dan memungkinkan Spark membatasi pembacaan pada partisi yang relevan.\n\nSetelah penyimpanan, dilakukan pemeriksaan struktur direktori, jumlah data, dan keberhasilan pembacaan kembali file Parquet.","metadata":{}},{"cell_type":"code","source":"# Lokasi penyimpanan hasil ETL di Kaggle\nparquet_path = \"/kaggle/working/hm_transactions_parquet\"\n\n# Menyimpan data dalam format Parquet dengan partisi bulanan\n(\n    transactions_final_df\n    .write\n    .mode(\"overwrite\")\n    .partitionBy(\"tahun_bulan\")\n    .parquet(parquet_path)\n)\n\nprint(\"Data berhasil ditulis ke:\", parquet_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:13:20.360032Z","iopub.execute_input":"2026-10-14T09:13:20.360413Z","iopub.status.idle":"2026-10-14T09:14:37.798628Z","shell.execute_reply.started":"2026-10-14T09:13:20.360387Z","shell.execute_reply":"2026-10-14T09:14:37.797655Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Membaca kembali hasil penyimpanan Parquet\ntransactions_parquet_df = spark.read.parquet(parquet_path)\n\nprint(\"=== VALIDASI HASIL LOAD ===\")\n\nprint(\"Jumlah baris sebelum Load :\", transactions_final_df.count())\nprint(\"Jumlah baris setelah Load :\", transactions_parquet_df.count())\n\nprint(\"\\n=== PARTISI YANG TERBENTUK ===\")\ntransactions_parquet_df.groupBy(\"tahun_bulan\") \\\n    .count() \\\n    .orderBy(\"tahun_bulan\") \\\n    .show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:15:52.514006Z","iopub.execute_input":"2026-10-14T09:15:52.514464Z","iopub.status.idle":"2026-10-14T09:16:27.772262Z","shell.execute_reply.started":"2026-10-14T09:15:52.514436Z","shell.execute_reply":"2026-10-14T09:16:27.771234Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.1 Hasil Validasi Penyimpanan Parquet\n\nHasil validasi menunjukkan bahwa penyimpanan data hasil ETL ke format Parquet berhasil dilakukan. Jumlah baris sebelum dan setelah penyimpanan tetap sebanyak 7.593.169 transaksi, sehingga tidak ditemukan kehilangan data selama proses *Load*.\n\nPartisi berdasarkan `tahun_bulan` menghasilkan enam kelompok penyimpanan, yaitu Januari hingga Juni 2020. Jumlah transaksi pada setiap partisi berkisar antara 1.001.859 transaksi pada Februari dan 1.764.507 transaksi pada Juni. Hasil ini menunjukkan bahwa pembagian partisi telah sesuai dengan periode analisis yang ditetapkan.","metadata":{}},{"cell_type":"markdown","source":"### 5.2 Perbandingan Format CSV dan Parquet\n\nPerbandingan dilakukan untuk mengevaluasi efisiensi penyimpanan dan pembacaan data menggunakan format CSV dan Parquet.\n\nAgar hasil perbandingan setara, kedua format menggunakan DataFrame hasil ETL yang sama, yaitu `transactions_final_df` dengan 7.593.169 baris dan struktur kolom yang identik.\n\nUkuran penyimpanan dihitung berdasarkan total ukuran file data yang dihasilkan, sedangkan waktu pembacaan diukur menggunakan operasi pembacaan dan penghitungan seluruh baris. Perbandingan ini bertujuan mengetahui perbedaan efisiensi kedua format pada lingkungan Apache Spark yang digunakan.","metadata":{}},{"cell_type":"code","source":"# Menyimpan data yang sama dalam format CSV\ncsv_path = \"/kaggle/working/hm_transactions_csv\"\n\n(\n    transactions_final_df\n    .write\n    .mode(\"overwrite\")\n    .option(\"header\", True)\n    .csv(csv_path)\n)\n\nprint(\"CSV pembanding berhasil disimpan ke:\", csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:19:17.933793Z","iopub.execute_input":"2026-10-14T09:19:17.935298Z","iopub.status.idle":"2026-10-14T09:20:36.680712Z","shell.execute_reply.started":"2026-10-14T09:19:17.935263Z","shell.execute_reply":"2026-10-14T09:20:36.679474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\ndef ukuran_data(path, ekstensi):\n    return sum(\n        file.stat().st_size\n        for file in Path(path).rglob(f\"*{ekstensi}\")\n        if file.is_file()\n    ) / (1024 ** 2)\n\nukuran_csv = ukuran_data(csv_path, \".csv\")\nukuran_parquet = ukuran_data(parquet_path, \".parquet\")\n\nprint(\"=== PERBANDINGAN UKURAN PENYIMPANAN ===\")\nprint(f\"Ukuran CSV     : {ukuran_csv:.2f} MiB\")\nprint(f\"Ukuran Parquet : {ukuran_parquet:.2f} MiB\")\n\nif ukuran_csv > 0:\n    penghematan = (ukuran_csv - ukuran_parquet) / ukuran_csv * 100\n    print(f\"Penghematan    : {penghematan:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:21:38.908579Z","iopub.execute_input":"2026-10-14T09:21:38.909269Z","iopub.status.idle":"2026-10-14T09:21:38.920948Z","shell.execute_reply.started":"2026-10-14T09:21:38.909236Z","shell.execute_reply":"2026-10-14T09:21:38.919894Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Perbandingan Ukuran Penyimpanan**\n\nBerdasarkan pengukuran terhadap 7.593.169 baris data hasil ETL dengan struktur kolom yang sama, format CSV membutuhkan ruang penyimpanan sebesar 1.277,31 MiB, sedangkan Parquet sebesar 175,46 MiB.\n\nPenggunaan Parquet menghasilkan penghematan ruang penyimpanan sebesar 86,26% dibandingkan CSV. Perbedaan ini menunjukkan bahwa format Parquet lebih efisien dalam menyimpan data transaksi H&M pada lingkungan pengujian yang digunakan. Efisiensi tersebut didukung oleh penyimpanan berbasis kolom dan kompresi data pada format Parquet.","metadata":{}},{"cell_type":"markdown","source":"### 5.3 Perbandingan Waktu Pembacaan Data\n\nPengujian waktu pembacaan dilakukan menggunakan Apache Spark terhadap data hasil ETL yang disimpan dalam format CSV dan Parquet. Kedua format memiliki jumlah baris dan cakupan data yang sama.\n\nWaktu pengujian dihitung sejak proses pembacaan file hingga operasi `count()` selesai dijalankan. Pengujian dilakukan untuk mengetahui perbedaan waktu pemrosesan kedua format pada lingkungan Kaggle yang digunakan.","metadata":{}},{"cell_type":"code","source":"import time\n\nprint(\"=== PENGUJIAN WAKTU BACA CSV ===\")\n\nmulai_csv = time.perf_counter()\n\ndf_csv_test = (\n    spark.read\n    .option(\"header\", True)\n    .csv(csv_path)\n)\n\njumlah_csv = df_csv_test.count()\n\nwaktu_csv = time.perf_counter() - mulai_csv\n\nprint(f\"Jumlah baris CSV : {jumlah_csv:,}\")\nprint(f\"Waktu baca CSV   : {waktu_csv:.2f} detik\")\n\n\nprint(\"\\n=== PENGUJIAN WAKTU BACA PARQUET ===\")\n\nmulai_parquet = time.perf_counter()\n\ndf_parquet_test = spark.read.parquet(parquet_path)\n\njumlah_parquet = df_parquet_test.count()\n\nwaktu_parquet = time.perf_counter() - mulai_parquet\n\nprint(f\"Jumlah baris Parquet : {jumlah_parquet:,}\")\nprint(f\"Waktu baca Parquet   : {waktu_parquet:.2f} detik\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:23:39.520217Z","iopub.execute_input":"2026-10-14T09:23:39.520989Z","iopub.status.idle":"2026-10-14T09:23:42.927249Z","shell.execute_reply.started":"2026-10-14T09:23:39.520951Z","shell.execute_reply":"2026-10-14T09:23:42.926073Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil Perbandingan Waktu Pembacaan**\n\nPengujian terhadap 7.593.169 baris data menunjukkan bahwa pembacaan CSV hingga operasi `count()` selesai membutuhkan waktu 2,97 detik, sedangkan Parquet membutuhkan waktu 0,43 detik. Dengan demikian, waktu pengujian Parquet sekitar 6,91 kali lebih singkat dibandingkan CSV.\n\nSelain itu, Parquet membutuhkan ruang penyimpanan sebesar 175,46 MiB, lebih kecil dibandingkan CSV sebesar 1.277,31 MiB, dengan penghematan sebesar 86,26%.\n\nHasil tersebut menunjukkan bahwa Parquet lebih efisien dalam penggunaan ruang penyimpanan dan operasi pembacaan yang diuji pada lingkungan Kaggle. Namun, pengujian hanya dilakukan satu kali menggunakan operasi `count()`, sehingga hasilnya belum dapat digeneralisasikan untuk seluruh jenis kueri atau kondisi komputasi.","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\n\nprint(\"=== FILE CSV ===\")\nfor file in list(Path(csv_path).rglob(\"*.csv\"))[:5]:\n    print(file)\n\nprint(\"\\n=== FILE PARQUET ===\")\nfor file in list(Path(parquet_path).rglob(\"*.parquet\"))[:5]:\n    print(file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:28:17.647425Z","iopub.execute_input":"2026-10-14T09:28:17.648338Z","iopub.status.idle":"2026-10-14T09:28:17.658422Z","shell.execute_reply.started":"2026-10-14T09:28:17.6483Z","shell.execute_reply":"2026-10-14T09:28:17.657417Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Analisis Data Menggunakan Spark SQL\n\nAnalisis dilakukan menggunakan Apache Spark SQL terhadap data hasil ETL yang telah disimpan dalam format Parquet. Tahap ini bertujuan menjawab tiga pertanyaan bisnis mengenai kelompok produk dengan total nilai `price` terbesar, perkembangan pembelian bulanan, serta perbandingan pembelian berdasarkan saluran penjualan.\n\nAnalisis dilaksanakan melalui kueri SQL yang mencakup pengelompokan data, penggabungan tabel, penyaringan kondisi, dan penggunaan *Common Table Expression* (CTE). Hasil kueri kemudian digunakan untuk menyusun interpretasi dan visualisasi data.","metadata":{}},{"cell_type":"code","source":"# Membaca hasil ETL dari Parquet\ndf_analisis = spark.read.parquet(parquet_path)\n\n# Membuat temporary view untuk Spark SQL\ndf_analisis.createOrReplaceTempView(\"hm_transactions\")\n\nprint(\"=== VALIDASI TEMPORARY VIEW ===\")\n\nspark.sql(\"\"\"\n    SELECT COUNT(*) AS jumlah_transaksi\n    FROM hm_transactions\n\"\"\").show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:52:22.452369Z","iopub.execute_input":"2026-10-14T09:52:22.452995Z","iopub.status.idle":"2026-10-14T09:52:24.524527Z","shell.execute_reply.started":"2026-10-14T09:52:22.452963Z","shell.execute_reply":"2026-10-14T09:52:24.523708Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 6.1 Analisis Kelompok Produk Berdasarkan Total Nilai Harga\n\nAnalisis pertama bertujuan mengidentifikasi kelompok produk dengan total nilai `price` terbesar selama Januari hingga Juni 2020. Pengelompokan dilakukan berdasarkan kolom `product_group_name` menggunakan perintah `GROUP BY`, sedangkan total nilai harga dihitung menggunakan fungsi agregasi `SUM(price)`.\n\nSelain total nilai harga, dihitung pula jumlah catatan pembelian menggunakan `COUNT(*)` untuk memberikan gambaran aktivitas pembelian pada setiap kelompok produk. Hasil analisis diurutkan berdasarkan total nilai `price` terbesar.\n\nNilai `price` yang digunakan merupakan nilai harga yang telah ditransformasikan dalam dataset, sehingga hasil penjumlahannya tidak ditafsirkan sebagai pendapatan dalam mata uang tertentu.","metadata":{}},{"cell_type":"code","source":"# SQL Query 1: Kelompok produk berdasarkan total nilai price\n\nquery_1_df = spark.sql(\"\"\"\n    SELECT\n        product_group_name,\n        COUNT(*) AS jumlah_pembelian,\n        ROUND(SUM(price), 4) AS total_nilai_price\n    FROM hm_transactions\n    GROUP BY product_group_name\n    ORDER BY total_nilai_price DESC\n\"\"\")\n\nprint(\"=== HASIL QUERY 1: KELOMPOK PRODUK ===\")\nquery_1_df.show(20, truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:54:12.174622Z","iopub.execute_input":"2026-10-14T09:54:12.175027Z","iopub.status.idle":"2026-10-14T09:54:14.070541Z","shell.execute_reply.started":"2026-10-14T09:54:12.175001Z","shell.execute_reply":"2026-10-14T09:54:14.069694Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil dan Interpretasi Query 1**\n\nHasil analisis menunjukkan bahwa *Garment Upper body* merupakan kelompok produk dengan total nilai `price` terbesar selama Januari–Juni 2020, yaitu 65.094,1658 dari 2.654.178 catatan pembelian. Posisi berikutnya ditempati oleh *Garment Lower body* dengan total nilai `price` sebesar 57.131,5438 dan *Garment Full body* sebesar 34.458,6584.\n\nTemuan ini menunjukkan bahwa kelompok pakaian bagian atas memberikan kontribusi nilai `price` terbesar dibandingkan kelompok produk lainnya dalam periode analisis. Oleh karena itu, kelompok tersebut dapat menjadi salah satu prioritas dalam evaluasi persediaan dan perencanaan penawaran produk. Namun, keputusan persediaan tetap memerlukan pertimbangan tambahan, seperti stok yang tersedia dan biaya pengadaan.","metadata":{}},{"cell_type":"markdown","source":"### 6.2 Analisis Perkembangan Pembelian Bulanan\n\nAnalisis kedua bertujuan mengidentifikasi perkembangan jumlah catatan pembelian dan total nilai `price` selama Januari hingga Juni 2020.\n\nPengelompokan dilakukan berdasarkan kolom `tahun_bulan` menggunakan perintah `GROUP BY`. Jumlah pembelian dihitung dengan `COUNT(*)`, sedangkan total nilai harga dihitung menggunakan `SUM(price)`. Hasil analisis diurutkan secara kronologis untuk melihat perubahan aktivitas pembelian antarbulan.","metadata":{}},{"cell_type":"code","source":"# SQL Query 2: Perkembangan pembelian bulanan\n\nquery_2_df = spark.sql(\"\"\"\n    SELECT\n        tahun_bulan,\n        COUNT(*) AS jumlah_pembelian,\n        ROUND(SUM(price), 4) AS total_nilai_price\n    FROM hm_transactions\n    GROUP BY tahun_bulan\n    ORDER BY tahun_bulan ASC\n\"\"\")\n\nprint(\"=== HASIL QUERY 2: PEMBELIAN BULANAN ===\")\nquery_2_df.show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:56:42.063323Z","iopub.execute_input":"2026-10-14T09:56:42.064287Z","iopub.status.idle":"2026-10-14T09:56:42.983556Z","shell.execute_reply.started":"2026-10-14T09:56:42.064255Z","shell.execute_reply":"2026-10-14T09:56:42.981988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil dan Interpretasi Query 2**\n\nHasil analisis menunjukkan bahwa jumlah catatan pembelian H&M selama Januari hingga Juni 2020 secara umum mengalami peningkatan, meskipun terdapat penurunan pada Februari. Jumlah pembelian terendah terjadi pada Februari sebanyak 1.001.859 catatan, sedangkan jumlah tertinggi terjadi pada Juni sebanyak 1.764.507 catatan.\n\nTotal nilai `price` tertinggi juga terjadi pada Juni, yaitu sebesar 43.055,3591. Namun, perkembangan jumlah pembelian tidak selalu sejalan dengan total nilai `price`. Hal ini terlihat pada Mei, ketika jumlah pembelian meningkat dari 1.340.882 menjadi 1.361.815 catatan, sementara total nilai `price` menurun dari 37.808,4857 menjadi 37.145,6944.\n\nTemuan ini menunjukkan pentingnya mempertimbangkan jumlah pembelian dan total nilai `price` secara bersamaan dalam mengevaluasi perkembangan aktivitas penjualan. Perubahan yang ditemukan bersifat deskriptif dan belum menunjukkan faktor penyebabnya.","metadata":{}},{"cell_type":"markdown","source":"### 6.3 Analisis Pembelian Berdasarkan Saluran Penjualan\n\nAnalisis ketiga bertujuan membandingkan jumlah catatan pembelian dan total nilai `price` berdasarkan saluran penjualan H&M selama Januari hingga Juni 2020.\n\nKolom `sales_channel_id` digunakan untuk membedakan saluran penjualan, dengan kode `1` menunjukkan toko fisik dan kode `2` menunjukkan toko online. Analisis dilakukan menggunakan `GROUP BY`, `COUNT(*)`, dan `SUM(price)` untuk mengetahui kontribusi masing-masing saluran terhadap aktivitas pembelian dan total nilai harga.","metadata":{}},{"cell_type":"code","source":"# SQL Query 3: Perbandingan saluran penjualan\n\nquery_3_df = spark.sql(\"\"\"\n    SELECT\n        sales_channel_id,\n        CASE\n            WHEN sales_channel_id = 1 THEN 'Toko Fisik'\n            WHEN sales_channel_id = 2 THEN 'Online'\n            ELSE 'Tidak Diketahui'\n        END AS saluran_penjualan,\n        COUNT(*) AS jumlah_pembelian,\n        ROUND(SUM(price), 4) AS total_nilai_price\n    FROM hm_transactions\n    GROUP BY sales_channel_id\n    ORDER BY sales_channel_id\n\"\"\")\n\nprint(\"=== HASIL QUERY 3: SALURAN PENJUALAN ===\")\nquery_3_df.show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T09:59:22.73844Z","iopub.execute_input":"2026-10-14T09:59:22.739428Z","iopub.status.idle":"2026-10-14T09:59:23.581984Z","shell.execute_reply.started":"2026-10-14T09:59:22.739397Z","shell.execute_reply":"2026-10-14T09:59:23.580533Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil dan Interpretasi Query 3**\n\nHasil analisis menunjukkan bahwa saluran online mencatat 5.815.015 pembelian atau sekitar 76,58% dari seluruh catatan pembelian selama Januari–Juni 2020. Sementara itu, toko fisik mencatat 1.778.154 pembelian atau sekitar 23,42%.\n\nDari sisi total nilai `price`, saluran online juga memberikan kontribusi lebih besar, yaitu 167.604,6284, dibandingkan toko fisik sebesar 38.265,5757.\n\nTemuan tersebut menunjukkan bahwa saluran online mendominasi aktivitas pembelian dan total nilai `price` selama periode analisis. Hasil ini dapat menjadi pertimbangan dalam evaluasi pengelolaan saluran penjualan, tetapi belum dapat digunakan untuk menyimpulkan perbedaan keuntungan karena informasi biaya tidak tersedia dalam dataset.","metadata":{}},{"cell_type":"markdown","source":"### 6.4 Analisis Kelompok Produk Berdasarkan Saluran Penjualan\n\nAnalisis tambahan dilakukan untuk mengidentifikasi kelompok produk dengan total nilai `price` terbesar pada masing-masing saluran penjualan. Tujuannya adalah mengetahui apakah kelompok produk yang memberikan kontribusi terbesar memiliki pola yang sama antara toko fisik dan online.\n\nKueri menggunakan operasi `JOIN` untuk menghubungkan transaksi dengan informasi produk berdasarkan `article_id`, `WHERE` untuk membatasi saluran penjualan yang dianalisis, serta `GROUP BY` untuk menghitung jumlah pembelian dan total nilai `price` pada setiap kombinasi saluran dan kelompok produk.","metadata":{}},{"cell_type":"code","source":"# Membuat temporary view referensi produk\narticles_typed_df.select(\n    \"article_id\",\n    \"product_group_name\"\n).createOrReplaceTempView(\"hm_articles\")\n\nprint(\"Temporary view 'hm_articles' berhasil dibuat.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:01:16.255664Z","iopub.execute_input":"2026-10-14T10:01:16.2562Z","iopub.status.idle":"2026-10-14T10:01:16.307189Z","shell.execute_reply.started":"2026-10-14T10:01:16.256171Z","shell.execute_reply":"2026-10-14T10:01:16.30615Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# SQL Query 4: Kelompok produk berdasarkan saluran penjualan\n\nquery_4_df = spark.sql(\"\"\"\n    SELECT\n        t.sales_channel_id,\n        CASE\n            WHEN t.sales_channel_id = 1 THEN 'Toko Fisik'\n            WHEN t.sales_channel_id = 2 THEN 'Online'\n        END AS saluran_penjualan,\n        a.product_group_name,\n        COUNT(*) AS jumlah_pembelian,\n        ROUND(SUM(t.price), 4) AS total_nilai_price\n    FROM hm_transactions t\n    INNER JOIN hm_articles a\n        ON t.article_id = a.article_id\n    WHERE t.sales_channel_id IN (1, 2)\n    GROUP BY\n        t.sales_channel_id,\n        a.product_group_name\n    ORDER BY\n        t.sales_channel_id,\n        total_nilai_price DESC\n\"\"\")\n\nprint(\"=== HASIL QUERY 4 ===\")\nquery_4_df.show(40, truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:01:52.359197Z","iopub.execute_input":"2026-10-14T10:01:52.359562Z","iopub.status.idle":"2026-10-14T10:01:57.239631Z","shell.execute_reply.started":"2026-10-14T10:01:52.359538Z","shell.execute_reply":"2026-10-14T10:01:57.238722Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil dan Interpretasi Query 4**\n\nHasil analisis menunjukkan bahwa *Garment Upper body*, *Garment Lower body*, dan *Garment Full body* merupakan tiga kelompok produk dengan total nilai `price` terbesar, baik pada saluran toko fisik maupun online.\n\nPada toko fisik, *Garment Upper body* menempati posisi pertama dengan total nilai `price` sebesar 15.504,2710, diikuti *Garment Lower body* sebesar 9.743,9269 dan *Garment Full body* sebesar 4.087,1714. Pada saluran online, ketiga kelompok tersebut juga menempati posisi teratas dengan total nilai `price` masing-masing sebesar 49.589,8949, 47.387,6170, dan 30.371,4869.\n\nTemuan ini menunjukkan kesamaan kelompok produk yang dominan pada kedua saluran, tetapi dengan besaran kontribusi yang berbeda. Hasil tersebut dapat menjadi pertimbangan dalam evaluasi komposisi produk pada masing-masing saluran penjualan.","metadata":{}},{"cell_type":"markdown","source":"### 6.5 Analisis Rata-Rata Nilai Harga pada Kelompok Produk dengan Pembelian Tinggi\n\nAnalisis kelima bertujuan membandingkan rata-rata nilai `price` pada kelompok produk yang memiliki aktivitas pembelian tinggi. Kelompok produk dipilih berdasarkan batas minimal 100.000 catatan pembelian selama Januari hingga Juni 2020.\n\nKueri menggunakan *Common Table Expression* (CTE) untuk menyusun hasil agregasi sementara, serta `HAVING` untuk menyaring kelompok produk berdasarkan jumlah pembelian. Rata-rata nilai `price` dihitung menggunakan fungsi `AVG(price)`.\n\nBatas 100.000 catatan digunakan sebagai kriteria analisis untuk memfokuskan perbandingan pada kelompok produk dengan jumlah pembelian relatif besar, bukan sebagai standar bisnis yang ditetapkan H&M.","metadata":{}},{"cell_type":"code","source":"# SQL Query 5: Rata-rata nilai price pada kelompok produk\n# dengan minimal 100.000 catatan pembelian\n\nquery_5_df = spark.sql(\"\"\"\n    WITH produk_aktif AS (\n        SELECT\n            product_group_name,\n            COUNT(*) AS jumlah_pembelian,\n            SUM(price) AS total_nilai_price,\n            AVG(price) AS rata_rata_price\n        FROM hm_transactions\n        GROUP BY product_group_name\n        HAVING COUNT(*) >= 100000\n    )\n    SELECT\n        product_group_name,\n        jumlah_pembelian,\n        ROUND(total_nilai_price, 4) AS total_nilai_price,\n        ROUND(rata_rata_price, 6) AS rata_rata_price\n    FROM produk_aktif\n    ORDER BY rata_rata_price DESC\n\"\"\")\n\nprint(\"=== HASIL QUERY 5: RATA-RATA PRICE KELOMPOK PRODUK ===\")\nquery_5_df.show(truncate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:04:19.504094Z","iopub.execute_input":"2026-10-14T10:04:19.504737Z","iopub.status.idle":"2026-10-14T10:04:20.875056Z","shell.execute_reply.started":"2026-10-14T10:04:19.504704Z","shell.execute_reply":"2026-10-14T10:04:20.873971Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Hasil dan Interpretasi Query 5**\n\nHasil analisis menunjukkan bahwa terdapat delapan kelompok produk dengan minimal 100.000 catatan pembelian selama Januari–Juni 2020. Dari kelompok tersebut, *Shoes* memiliki rata-rata nilai `price` tertinggi sebesar 0,036416 dengan 170.780 catatan pembelian. Posisi berikutnya ditempati oleh *Garment Full body* sebesar 0,035855 dan *Garment Lower body* sebesar 0,033458.\n\nSementara itu, *Garment Upper body* memiliki jumlah pembelian terbanyak, yaitu 2.654.178 catatan, tetapi rata-rata nilai `price` sebesar 0,024525 menempatkannya pada urutan keempat.\n\nTemuan ini menunjukkan bahwa tingginya jumlah pembelian tidak selalu berkaitan dengan tingginya rata-rata nilai harga. Oleh karena itu, jumlah pembelian, total nilai `price`, dan rata-rata nilai `price` perlu dipertimbangkan secara bersama-sama dalam mengevaluasi kinerja kelompok produk.","metadata":{}},{"cell_type":"markdown","source":"### 6.6 Visualisasi Hasil Analisis\n\nVisualisasi hasil analisis Spark SQL disajikan melalui tiga grafik yang menggambarkan perkembangan pembelian bulanan, total nilai `price` berdasarkan kelompok produk, dan distribusi pembelian berdasarkan saluran penjualan H&M selama Januari–Juni 2020.","metadata":{}},{"cell_type":"markdown","source":"#### 6.6.1 Tren Pembelian Bulanan\n\nGrafik *area line chart* menampilkan perkembangan jumlah catatan pembelian H&M setiap bulan selama Januari–Juni 2020 berdasarkan hasil SQL Query 2.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport seaborn as sns\nimport numpy as np\nimport pandas as pd\n\nsns.set_theme(style=\"whitegrid\")\n\nNAVY = \"#172554\"\nBLUE = \"#2563EB\"\nPURPLE = \"#7C3AED\"\nTEAL = \"#0D9488\"\nGRAY = \"#64748B\"\n\nplt.rcParams.update({\n    \"figure.dpi\": 130,\n    \"font.family\": \"DejaVu Sans\",\n    \"font.size\": 11,\n    \"axes.titlesize\": 17,\n    \"axes.titleweight\": \"bold\",\n    \"axes.labelcolor\": NAVY,\n    \"text.color\": NAVY,\n    \"xtick.color\": GRAY,\n    \"ytick.color\": GRAY,\n    \"figure.facecolor\": \"white\",\n    \"axes.facecolor\": \"white\"\n})\n\ndef format_angka(angka, desimal=0):\n    teks = f\"{angka:,.{desimal}f}\"\n    return teks.replace(\",\", \"X\").replace(\".\", \",\").replace(\"X\", \".\")\n\nprint(\"Pengaturan visualisasi berhasil.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:15:25.411478Z","iopub.execute_input":"2026-10-14T10:15:25.412095Z","iopub.status.idle":"2026-10-14T10:15:28.448655Z","shell.execute_reply.started":"2026-10-14T10:15:25.412064Z","shell.execute_reply":"2026-10-14T10:15:28.447679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualisasi 1: Tren Pembelian Bulanan\n\nmonthly_pd = (\n    query_2_df.toPandas()\n    .sort_values(\"tahun_bulan\")\n)\n\nbulan = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"Mei\", \"Jun\"]\njumlah = monthly_pd[\"jumlah_pembelian\"].to_numpy()\nx = np.arange(len(bulan))\n\nfig, ax = plt.subplots(figsize=(12, 6.5))\n\nax.fill_between(x, jumlah, color=BLUE, alpha=0.12)\n\nax.plot(\n    x, jumlah,\n    color=BLUE,\n    linewidth=3,\n    marker=\"o\",\n    markersize=9,\n    markerfacecolor=\"white\",\n    markeredgewidth=2.5,\n    zorder=3\n)\n\nidx_max = np.argmax(jumlah)\n\nax.scatter(\n    x[idx_max], jumlah[idx_max],\n    s=220, color=TEAL,\n    edgecolor=\"white\", linewidth=2,\n    zorder=5\n)\n\nfor i, nilai in enumerate(jumlah):\n    ax.annotate(\n        f\"{nilai/1_000_000:.2f} jt\",\n        (x[i], nilai),\n        xytext=(0, 15),\n        textcoords=\"offset points\",\n        ha=\"center\",\n        fontsize=10,\n        fontweight=\"bold\",\n        color=TEAL if i == idx_max else NAVY\n    )\n\nax.set_title(\n    \"Tren Pembelian Bulanan H&M\",\n    loc=\"left\", pad=35\n)\n\nax.text(\n    0, 1.035,\n    \"Jumlah catatan pembelian | Januari - Juni 2020\",\n    transform=ax.transAxes,\n    fontsize=10, color=GRAY\n)\n\nax.set_xticks(x)\nax.set_xticklabels(bulan)\nax.set_ylabel(\"Jumlah Catatan Pembelian\")\n\nax.yaxis.set_major_formatter(\n    ticker.FuncFormatter(\n        lambda v, pos: f\"{v/1_000_000:.1f} jt\"\n    )\n)\n\nax.set_ylim(0, max(jumlah) * 1.25)\n\nax.grid(axis=\"y\", linestyle=\"--\", alpha=0.20)\nax.grid(axis=\"x\", visible=False)\n\nsns.despine(ax=ax, left=True, bottom=True)\nax.tick_params(axis=\"both\", length=0, pad=10)\n\nfig.text(\n    0.12, 0.01,\n    \"Sumber: H&M Personalized Fashion Recommendations | Spark SQL Query 2\",\n    fontsize=9, color=GRAY\n)\n\nplt.tight_layout(rect=[0, 0.04, 1, 1])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:15:46.300282Z","iopub.execute_input":"2026-10-14T10:15:46.303304Z","iopub.status.idle":"2026-10-14T10:15:47.860685Z","shell.execute_reply.started":"2026-10-14T10:15:46.303182Z","shell.execute_reply":"2026-10-14T10:15:47.859408Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Interpretasi:** Jumlah pembelian H&M selama Januari–Juni 2020 cenderung meningkat meskipun sempat menurun pada Februari. Jumlah pembelian tertinggi terjadi pada Juni sebanyak 1.764.507 catatan, sedangkan terendah pada Februari sebanyak 1.001.859 catatan.","metadata":{}},{"cell_type":"markdown","source":"#### 6.6.2 Total Nilai Price Berdasarkan Kelompok Produk\n\nGrafik *horizontal bar chart* menampilkan sepuluh kelompok produk H&M dengan total nilai `price` terbesar selama Januari–Juni 2020 berdasarkan hasil SQL Query 1.","metadata":{}},{"cell_type":"code","source":"# Visualisasi 2: Top 10 Kelompok Produk\n\nproduk_pd = (\n    query_1_df\n    .orderBy(\"total_nilai_price\", ascending=False)\n    .limit(10)\n    .toPandas()\n    .sort_values(\"total_nilai_price\", ascending=True)\n)\n\nkategori = produk_pd[\"product_group_name\"]\nnilai = produk_pd[\"total_nilai_price\"]\n\nwarna = sns.color_palette(\n    \"Purples\", n_colors=len(produk_pd) + 4\n)[4:]\n\nfig, ax = plt.subplots(figsize=(12, 7))\n\nbars = ax.barh(\n    kategori, nilai,\n    color=warna,\n    height=0.68,\n    edgecolor=\"none\"\n)\n\nbars[-1].set_color(PURPLE)\n\nfor bar, val in zip(bars, nilai):\n    ax.text(\n        bar.get_width() + max(nilai) * 0.012,\n        bar.get_y() + bar.get_height() / 2,\n        format_angka(val, 2),\n        va=\"center\",\n        fontsize=10,\n        fontweight=\"bold\",\n        color=NAVY\n    )\n\nax.set_title(\n    \"Top 10 Kelompok Produk H&M\",\n    loc=\"left\", pad=35\n)\n\nax.text(\n    0, 1.035,\n    \"Berdasarkan total nilai price | Januari - Juni 2020\",\n    transform=ax.transAxes,\n    fontsize=10, color=GRAY\n)\n\nax.set_xlabel(\"Total Nilai Price\")\nax.set_ylabel(\"\")\n\nax.xaxis.set_major_formatter(\n    ticker.FuncFormatter(\n        lambda v, pos: format_angka(v, 0)\n    )\n)\n\nax.set_xlim(0, max(nilai) * 1.24)\n\nax.grid(axis=\"x\", linestyle=\"--\", alpha=0.18)\nax.grid(axis=\"y\", visible=False)\n\nsns.despine(ax=ax, left=True, bottom=True)\nax.tick_params(axis=\"both\", length=0, pad=8)\n\nfig.text(\n    0.12, 0.01,\n    \"Sumber: H&M Personalized Fashion Recommendations | Spark SQL Query 1\",\n    fontsize=9, color=GRAY\n)\n\nplt.tight_layout(rect=[0, 0.04, 1, 1])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:16:14.138011Z","iopub.execute_input":"2026-10-14T10:16:14.138507Z","iopub.status.idle":"2026-10-14T10:16:15.6303Z","shell.execute_reply.started":"2026-10-14T10:16:14.138477Z","shell.execute_reply":"2026-10-14T10:16:15.629121Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Interpretasi:** *Garment Upper body* memiliki total nilai `price` terbesar, yaitu 65.094,1658, diikuti *Garment Lower body* sebesar 57.131,5438 dan *Garment Full body* sebesar 34.458,6584. Hal ini menunjukkan bahwa kelompok produk pakaian mendominasi total nilai `price` selama periode analisis.","metadata":{}},{"cell_type":"markdown","source":"#### 6.6.3 Distribusi Pembelian Berdasarkan Saluran Penjualan\n\nGrafik *donut chart* menampilkan persentase jumlah catatan pembelian melalui toko fisik dan online selama Januari–Juni 2020 berdasarkan hasil SQL Query 3.","metadata":{}},{"cell_type":"code","source":"# Visualisasi 3: Distribusi Saluran Penjualan\n\nchannel_pd = (\n    query_3_df\n    .orderBy(\"sales_channel_id\")\n    .toPandas()\n)\n\nlabels = channel_pd[\"saluran_penjualan\"].tolist()\njumlah = channel_pd[\"jumlah_pembelian\"].to_numpy()\ntotal_price = channel_pd[\"total_nilai_price\"].to_numpy()\n\ncolors = [\"#93C5FD\", \"#0D9488\"]\n\nfig, ax = plt.subplots(figsize=(11, 7))\n\nwedges, texts, autotexts = ax.pie(\n    jumlah,\n    colors=colors,\n    startangle=90,\n    counterclock=False,\n    autopct=\"%1.1f%%\",\n    pctdistance=0.80,\n    wedgeprops={\n        \"width\": 0.35,\n        \"edgecolor\": \"white\",\n        \"linewidth\": 4\n    },\n    textprops={\n        \"fontsize\": 12,\n        \"fontweight\": \"bold\",\n        \"color\": NAVY\n    }\n)\n\nax.text(\n    0, 0.12,\n    \"TOTAL PEMBELIAN\",\n    ha=\"center\",\n    fontsize=10,\n    color=GRAY,\n    fontweight=\"bold\"\n)\n\nax.text(\n    0, -0.08,\n    format_angka(sum(jumlah)),\n    ha=\"center\",\n    fontsize=19,\n    fontweight=\"bold\",\n    color=NAVY\n)\n\nax.set_title(\n    \"Distribusi Pembelian Berdasarkan Saluran Penjualan\",\n    pad=25\n)\n\nlegend_labels = [\n    (\n        f\"{labels[i]} | \"\n        f\"{format_angka(jumlah[i])} pembelian | \"\n        f\"Total price: {format_angka(total_price[i], 2)}\"\n    )\n    for i in range(len(labels))\n]\n\nax.legend(\n    wedges,\n    legend_labels,\n    loc=\"lower center\",\n    bbox_to_anchor=(0.5, -0.14),\n    ncol=1,\n    frameon=False,\n    fontsize=10,\n    labelspacing=1.2\n)\n\nax.set_aspect(\"equal\")\n\nfig.text(\n    0.12, 0.01,\n    \"Sumber: H&M Personalized Fashion Recommendations | Spark SQL Query 3\",\n    fontsize=9, color=GRAY\n)\n\nplt.tight_layout(rect=[0, 0.05, 1, 1])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:16:49.865698Z","iopub.execute_input":"2026-10-14T10:16:49.866111Z","iopub.status.idle":"2026-10-14T10:16:50.779919Z","shell.execute_reply.started":"2026-10-14T10:16:49.866081Z","shell.execute_reply":"2026-10-14T10:16:50.779133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Interpretasi:** Saluran online mendominasi aktivitas pembelian dengan kontribusi sebesar 76,58% atau 5.815.015 catatan, sedangkan toko fisik sebesar 23,42% atau 1.778.154 catatan. Hal ini menunjukkan bahwa sebagian besar pembelian H&M selama periode analisis berlangsung melalui saluran online.","metadata":{}},{"cell_type":"markdown","source":"## 7. Etika, Privasi, dan Tata Kelola Data\n\n### 7.1 Identifikasi Risiko Etika dan Privasi\n\nDataset H&M Personalized Fashion Recommendations memuat informasi transaksi, produk, dan pelanggan. Pengolahan data tersebut memiliki beberapa risiko privasi yang perlu diperhatikan.\n\n1. **Identifikasi pelanggan:** Kolom `customer_id` merupakan pengenal unik yang memungkinkan aktivitas pembelian dikaitkan dengan pelanggan tertentu.\n2. **Informasi demografis:** Kolom `age` dan `postal_code` berpotensi meningkatkan risiko identifikasi apabila digabungkan dengan informasi lain.\n3. **Riwayat pembelian:** Data transaksi dapat digunakan untuk mengamati pola pembelian pelanggan, sehingga perlu dibatasi penggunaannya sesuai tujuan analisis.\n4. **Penyalahgunaan data:** Penyimpanan dan pembagian data tanpa pengendalian akses dapat meningkatkan risiko kebocoran informasi pelanggan.\n\nOleh karena itu, pengolahan data dilakukan dengan membatasi atribut pelanggan yang digunakan serta menerapkan perlindungan terhadap pengenal pelanggan pada hasil ETL.","metadata":{}},{"cell_type":"markdown","source":"### 7.2 Implementasi Perlindungan Privasi Data\n\nPerlindungan privasi diterapkan melalui teknik *pseudonymization* menggunakan algoritma hashing SHA-256 terhadap kolom `customer_id`. Proses ini menghasilkan kolom `customer_hash`, kemudian kolom `customer_id` asli dihapus dari dataset hasil ETL.\n\nSelain itu, atribut `postal_code`, `FN`, dan `Active` tidak disertakan dalam dataset transaksi akhir karena tidak diperlukan untuk menjawab pertanyaan bisnis.\n\nPenerapan tersebut bertujuan mengurangi paparan pengenal pelanggan dalam dataset analisis. Namun, hashing tanpa kunci rahasia tidak menjamin anonimitas sepenuhnya karena nilai hash yang sama masih dapat digunakan untuk menghubungkan catatan milik pelanggan yang sama.","metadata":{}},{"cell_type":"code","source":"# TAHAP 7: VALIDASI PERLINDUNGAN PRIVASI\n\ndf_privasi = spark.read.parquet(parquet_path)\n\nkolom_asli = \"customer_id\"\nkolom_hash = \"customer_hash\"\n\nprint(\"=== VALIDASI PRIVASI DATA ===\")\n\nprint(\n    \"Kolom customer_id tersedia:\",\n    kolom_asli in df_privasi.columns\n)\n\nprint(\n    \"Kolom customer_hash tersedia:\",\n    kolom_hash in df_privasi.columns\n)\n\nprint(\n    \"Kolom postal_code tersedia:\",\n    \"postal_code\" in df_privasi.columns\n)\n\nprint(\n    \"Kolom FN tersedia:\",\n    \"FN\" in df_privasi.columns\n)\n\nprint(\n    \"Kolom Active tersedia:\",\n    \"Active\" in df_privasi.columns\n)\n\n# Memeriksa nilai hash yang kosong\ndf_privasi.select(\n    F.count(\"*\").alias(\"jumlah_baris\"),\n    F.sum(\n        F.when(F.col(\"customer_hash\").isNull(), 1)\n         .otherwise(0)\n    ).alias(\"hash_null\")\n).show()\n\n# Contoh hash tanpa menampilkan identitas asli\ndf_privasi.select(\"customer_hash\").show(\n    5, truncate=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T10:21:46.803618Z","iopub.execute_input":"2026-10-14T10:21:46.804833Z","iopub.status.idle":"2026-10-14T10:21:48.268752Z","shell.execute_reply.started":"2026-10-14T10:21:46.804786Z","shell.execute_reply":"2026-10-14T10:21:48.267837Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 7.3 Tata Kelola dan Keamanan Data\n\nPengelolaan data pada penelitian ini memperhatikan beberapa aspek berikut.\n\n1. **Pengendalian akses:** Dataset mentah yang memuat pengenal pelanggan sebaiknya hanya dapat diakses oleh pihak yang berwenang. Dataset hasil ETL digunakan untuk keperluan analisis.\n2. **Pembatasan penggunaan:** Data digunakan untuk analisis kelompok produk, perkembangan pembelian, dan saluran penjualan sesuai tujuan yang telah ditetapkan.\n3. **Penyimpanan dan retensi:** Data hasil ETL disimpan dalam format Parquet dengan partisi bulanan. File sementara dan salinan data yang tidak diperlukan sebaiknya dihapus setelah proses analisis dan penilaian selesai, sesuai ketentuan penyimpanan yang berlaku.\n4. **Pencegahan kebocoran data:** Pengenal pelanggan asli tidak disertakan dalam dataset akhir. Hasil analisis disajikan dalam bentuk agregasi untuk mengurangi pengungkapan informasi tingkat individu.\n5. **Keterbatasan perlindungan:** Penggunaan SHA-256 tanpa kunci rahasia masih memungkinkan keterkaitan antartransaksi melalui nilai hash yang sama. Oleh karena itu, pembatasan akses tetap diperlukan.\n\nKetentuan tersebut merupakan langkah pengelolaan dan rekomendasi keamanan data. Implementasi teknis yang telah dilakukan dalam ETL mencakup hashing pengenal pelanggan dan pengurangan atribut yang tidak diperlukan.","metadata":{}},{"cell_type":"markdown","source":"### 7.4 Kesimpulan Etika, Privasi, dan Tata Kelola Data\n\nPerlindungan privasi dalam proses ETL dilakukan melalui hashing SHA-256 pada `customer_id`, penghapusan pengenal pelanggan asli, serta pembatasan atribut yang disertakan dalam dataset akhir. Pendekatan ini mengurangi paparan informasi pelanggan tanpa menghilangkan kemampuan data untuk dianalisis secara agregat.\n\nMeskipun demikian, hasil hashing masih bersifat pseudonim sehingga pengendalian akses, pembatasan penggunaan, dan pengelolaan penyimpanan tetap diperlukan untuk menjaga keamanan data.","metadata":{}}]}