Bagian A: Pilihan Ganda

Pilih satu jawaban yang paling tepat.

A1. Data GPS 2 juta pengemudi ojek daring yang diperbarui setiap 3 detik paling menonjol pada karakteristik …

(a) Variety (b) Velocity (c) Veracity (d) Value

Jawaban: (b) Velocity

Alasan:

Karakteristik Velocity merujuk pada kecepatan aliran data yang dihasilkan dan diperbarui secara real-time atau dalam interval frekuensi yang sangat tinggi, seperti pembaruan posisi GPS dari 2 juta pengemudi setiap 3 detik.

A2. Basis data yang paling tepat untuk menyimpan sesi login dengan masa berlaku 30 menit adalah …

(a) Neo4j (b) MongoDB (c) Redis (d) HDFS

Jawaban: (c) Redis

Alasan:

Redis merupakan basis data NoSQL berarsitektur key-value in-memory yang sangat ideal dan responsif untuk menyimpan data sementara (transient), seperti sesi login yang memerlukan batas waktu berlaku (time-to-live).

A3. Pada MongoDB, tahap aggregation pipeline yang memecah elemen array menjadi dokumen terpisah adalah …

(a) $match (b) $group (c) $unwind (d) $project

Jawaban: (c) $unwind

Alasan:

Operasi $unwind dalam aggregation pipeline MongoDB berfungsi mendekomposisi setiap elemen di dalam kolom berjenis himpunan (array) agar menjadi dokumen terpisah untuk dianalisis secara mandiri.

A4. Berkas 700 MB diunggah ke HDFS dengan blok 128 MB dan replikasi 3. Jumlah blok dan ruang disk yang terpakai adalah …

(a) 5 blok, 700 MB (b) 6 blok, 2.100 MB (c) 6 blok, 2.304 MB (d) 18 blok, 2.304 MB

Jawaban: (b) 6 blok, 2.100 MB

Alasan:

Berkas berukuran 700 MB yang dibagi dengan ukuran blok HDFS 128 MB akan menghasilkan 6 blok logis, yaitu 5 blok utuh berukuran 128 MB dan 1 blok sisa berukuran 60 MB.

Dengan faktor replikasi 3, total ruang penyimpanan fisik yang terpakai pada klaster adalah:

\[ 700 \text{ MB} \times 3 = 2.100 \text{ MB} \]

A5. Komponen HDFS yang menyimpan metadata lokasi blok adalah …

(a) DataNode (b) NameNode (c) Secondary NameNode (d) ResourceManager

Jawaban: (b) NameNode

Alasan:

NameNode bertindak sebagai simpul master pada arsitektur HDFS yang mengelola dan menyimpan seluruh metadata struktur direktori, peta berkas, serta informasi lokasi fisik blok data di DataNode.

A6. Operasi Spark berikut yang merupakan aksi (action) adalah …

(a) map (b) filter (c) reduceByKey (d) count

Jawaban: (d) count

Alasan:

Operasi count merupakan sebuah aksi (action) pada Apache Spark yang memicu eksekusi rangkaian operasi RDD/DAG dan mengembalikan nilai jumlah elemen ke program driver.

A7. Untuk menjumlahkan nilai per kunci pada pair RDD besar, operasi yang paling efisien adalah …

(a) groupByKey().mapValues(sum) (b) reduceByKey(lambda a, b: a + b) (c) collect() lalu dijumlah di Python (d) sortByKey()

Jawaban: (b) reduceByKey(lambda a, b: a + b)

Alasan:

Operasi reduceByKey melakukan penggabungan data awal di tingkat partisi lokal (map-side combine) sebelum data dikirimkan antar-mesin melalui proses shuffle, sehingga lebih efisien dalam menghemat konsumsi jaringan klaster.

A8. Mengapa skema eksplisit lebih disarankan daripada inferSchema=True pada pipeline produksi?

(a) Skema membuat data terkompresi (b) Lebih cepat, tidak salah tebak tipe, dan terdokumentasi (c) inferSchema tidak dapat membaca CSV (d) Skema wajib untuk Parquet

Jawaban: (b) Lebih cepat, tidak salah tebak tipe, dan terdokumentasi

Alasan:

Pendefinisian skema secara eksplisit mencegah Spark melakukan pemindaian awal data hanya untuk menebak tipe kolom (inferSchema), sehingga dapat mempercepat proses ingestion dan menghindari kesalahan pembacaan tipe data.

A9. Topic Kafka memiliki 4 partisi dan consumer group memiliki 6 konsumen. Berapa konsumen yang aktif membaca?

(a) 2 (b) 4 (c) 6 (d) 24

Jawaban: (b) 4

Alasan:

Dalam arsitektur Apache Kafka, satu partisi dalam consumer group hanya dapat dibaca oleh maksimal satu konsumen aktif pada waktu yang sama. Karena topic hanya memiliki 4 partisi, maka maksimal 4 konsumen dapat bekerja secara bersamaan, sedangkan 2 konsumen sisanya berada pada posisi siaga (idle).

A10. Cara yang benar membuat histogram kolom jarak_km dari 2 miliar baris adalah …

(a) df.toPandas()["jarak_km"].hist() (b) df.collect() lalu plt.hist (c) Menghitung jumlah per bin dengan groupBy di Spark (d) Mengambil 10 baris pertama

Jawaban: (c) Menghitung jumlah per bin dengan groupBy di Spark, lalu menggambar hasilnya

Alasan:

Sesuai prinsip Large-Scale Exploratory Data Analysis (EDA), proses pengelompokan (binning) dan agregasi data berskala besar harus dilakukan di dalam klaster terdistribusi Spark. Dengan demikian, hanya tabel ringkasan berukuran kecil yang dikirimkan ke komputer driver untuk divisualisasikan menjadi histogram.


Bagian B: Esai Singkat

B1. Jelaskan perbedaan scale-up dan scale-out, dan jelaskan mengapa fase shuffle pada MapReduce/Spark merupakan bagian yang paling mahal.

Jawaban:

B2. Sebuah marketplace membutuhkan: (i) katalog produk dengan atribut berbeda per kategori, (ii) keranjang belanja sementara, (iii) rekomendasi “teman Anda juga membeli”. Pilih basis data NoSQL untuk masing-masing dan jelaskan alasannya.

Jawaban:

B3. Jelaskan konsep lazy evaluation dan lineage pada Spark, serta manfaat masing-masing.

Jawaban:

B4. Jelaskan perbedaan jaminan pengiriman at-most-once dan at-least-once, dan bagaimana checkpoint pada Spark Structured Streaming membantu mencegah data hilang atau terproses ganda.

Jawaban:


Bagian C: Praktik Coding – Studi Kasus Dompet Digital “BayarYuk”

BayarYuk adalah aplikasi dompet digital. Tim data diminta menganalisis data merchant (tersimpan di MongoDB), log transaksi (berkas teks), dan aliran transaksi baru (streaming). Jalankan chunk berikut untuk menyiapkan Spark dan membangkitkan dataset ujian.

# ============================================================
# SETUP SPARK BAYARYUK
# ============================================================

# 1. Mengimpor modul sistem
import os
import sys
import subprocess

# 2. Menentukan lokasi Java dan Hadoop
JAVA_HOME = r"C:\Program Files\Eclipse Adoptium\jdk-17.0.20.101-hotspot"

HADOOP_HOME = r"C:\hadoop-3.5.0-bin\hadoop-3.5.0"

# 3. Mengatur environment Java dan Hadoop
os.environ["JAVA_HOME"] = JAVA_HOME
os.environ["HADOOP_HOME"] = HADOOP_HOME

os.environ["PATH"] = (
    os.path.join(JAVA_HOME, "bin")
    + os.pathsep
    + os.path.join(HADOOP_HOME, "bin")
    + os.pathsep
    + os.environ["PATH"]
)

# 4. Menggunakan interpreter Python yang sama
#    untuk Spark Driver dan Worker
os.environ["PYSPARK_PYTHON"] = sys.executable
os.environ["PYSPARK_DRIVER_PYTHON"] = sys.executable

# 5. Mengatur alamat jaringan lokal
os.environ["SPARK_LOCAL_IP"] = "127.0.0.1"

# 6. Mengimpor modul PySpark
from pyspark.sql import SparkSession
import pyspark.java_gateway as jg

# 7. Memperbaiki masalah Windows Stdout/Stderr
#    pada pemanggilan Java Gateway dari Reticulate
_popen_asli = jg.Popen

def popen_aman(command, **kwargs):
    kwargs.setdefault("stdout", subprocess.DEVNULL)
    kwargs.setdefault("stderr", subprocess.DEVNULL)

    return _popen_asli(command, **kwargs)

# 8. Membuat SparkSession
jg.Popen = popen_aman

try:
    spark = (
        SparkSession.builder
        .master("local[*]")
        .appName("BigData-P08")
        .config("spark.ui.showConsoleProgress", "false")
        .config("spark.sql.shuffle.partitions", "8")
        .config("spark.driver.memory", "2g")
        .config("spark.driver.host", "127.0.0.1")
        .config("spark.driver.bindAddress", "127.0.0.1")
        .config("spark.pyspark.python", sys.executable)
        .config("spark.pyspark.driver.python", sys.executable)
        .config(
            "spark.executorEnv.PYSPARK_PYTHON",
            sys.executable
        )
        .config(
            "spark.driver.extraJavaOptions",
            "-Dhadoop.home.dir=C:/hadoop-3.5.0-bin/hadoop-3.5.0 "
            "-Djava.library.path=C:/hadoop-3.5.0-bin/hadoop-3.5.0/bin"
        )
        .config(
            "spark.executor.extraJavaOptions",
            "-Dhadoop.home.dir=C:/hadoop-3.5.0-bin/hadoop-3.5.0 "
            "-Djava.library.path=C:/hadoop-3.5.0-bin/hadoop-3.5.0/bin"
        )
        .getOrCreate()
    )

finally:
    # Mengembalikan fungsi Popen seperti semula
    jg.Popen = _popen_asli

# 9. Mengatur tingkat log Spark
spark.sparkContext.setLogLevel("ERROR")

# 10. Menyimpan referensi SparkContext
sc = spark.sparkContext

# 11. Menampilkan informasi pemeriksaan Spark
print("=== PEMERIKSAAN SPARK ===")
## === PEMERIKSAAN SPARK ===
print("Spark versi:", spark.version)
## Spark versi: 4.3.0
print("Master:", sc.master)
## Master: local[*]
print("Jumlah core paralel:", sc.defaultParallelism)
## Jumlah core paralel: 8
print("Python executable:", sys.executable)
## Python executable: C:\Users\DWIMAN~1\AppData\Local\Programs\Python\PYTHON~1\python.exe
print("Java Home:", os.environ["JAVA_HOME"])
## Java Home: C:\Program Files\Eclipse Adoptium\jdk-17.0.20.101-hotspot
print("Hadoop Home:", os.environ["HADOOP_HOME"])
## Hadoop Home: C:\hadoop-3.5.0-bin\hadoop-3.5.0
print("Spark tersedia:", spark is not None)
## Spark tersedia: True

Interpretasi:

Berdasarkan hasil pemeriksaan, Apache Spark berhasil dijalankan dengan dukungan 8 core prosesor. Python, Java, dan Hadoop juga telah terdeteksi dengan baik. Dengan demikian, lingkungan pengolahan data telah siap digunakan untuk menjalankan analisis Big Data.


# Mengimpor pustaka yang dipakai selama ujian.
import numpy as np
import pandas as pd
import os, json, tempfile
import mongomock
from pyspark.sql import functions as F
from pyspark.sql.window import Window

# Seed tetap agar semua peserta mendapat data yang sama.
rng = np.random.default_rng(2026)
folder = tempfile.mkdtemp()

# 1) Data merchant (dokumen MongoDB)
kategori = ["Kuliner", "Retail", "Transportasi", "Hiburan", "Kesehatan"]
kota = ["Jakarta", "Bandung", "Surabaya", "Medan", "Makassar"]
merchant = []
for i in range(1, 201):
    # Setiap merchant memiliki 0-2 promo acak.
    d = {"_id": f"M{i:03d}", "nama": f"Merchant {i}",
         "kategori": str(rng.choice(kategori)),
         "kota": str(rng.choice(kota)),
         "rating": float(np.round(rng.uniform(3.0, 5.0), 1)),
         "promo": [str(p) for p in rng.choice(["cashback", "diskon", "gratis_ongkir", "poin"],
                                                size=rng.integers(0, 3), replace=False)]}

    # 30% merchant memiliki field jam_buka (skema fleksibel).
    if rng.random() < 0.3:
        d["jam_buka"] = {"buka": "08:00", "tutup": str(rng.choice(["21:00", "22:00", "23:59"]))}
    merchant.append(d)

# Menyimpan merchant ke MongoDB (simulator).
db = mongomock.MongoClient()["bayaryuk"]
db["merchant"].insert_many(merchant)
## InsertManyResult(['M001', 'M002', 'M003', 'M004', 'M005', 'M006', 'M007', 'M008', 'M009', 'M010', 'M011', 'M012', 'M013', 'M014', 'M015', 'M016', 'M017', 'M018', 'M019', 'M020', 'M021', 'M022', 'M023', 'M024', 'M025', 'M026', 'M027', 'M028', 'M029', 'M030', 'M031', 'M032', 'M033', 'M034', 'M035', 'M036', 'M037', 'M038', 'M039', 'M040', 'M041', 'M042', 'M043', 'M044', 'M045', 'M046', 'M047', 'M048', 'M049', 'M050', 'M051', 'M052', 'M053', 'M054', 'M055', 'M056', 'M057', 'M058', 'M059', 'M060', 'M061', 'M062', 'M063', 'M064', 'M065', 'M066', 'M067', 'M068', 'M069', 'M070', 'M071', 'M072', 'M073', 'M074', 'M075', 'M076', 'M077', 'M078', 'M079', 'M080', 'M081', 'M082', 'M083', 'M084', 'M085', 'M086', 'M087', 'M088', 'M089', 'M090', 'M091', 'M092', 'M093', 'M094', 'M095', 'M096', 'M097', 'M098', 'M099', 'M100', 'M101', 'M102', 'M103', 'M104', 'M105', 'M106', 'M107', 'M108', 'M109', 'M110', 'M111', 'M112', 'M113', 'M114', 'M115', 'M116', 'M117', 'M118', 'M119', 'M120', 'M121', 'M122', 'M123', 'M124', 'M125', 'M126', 'M127', 'M128', 'M129', 'M130', 'M131', 'M132', 'M133', 'M134', 'M135', 'M136', 'M137', 'M138', 'M139', 'M140', 'M141', 'M142', 'M143', 'M144', 'M145', 'M146', 'M147', 'M148', 'M149', 'M150', 'M151', 'M152', 'M153', 'M154', 'M155', 'M156', 'M157', 'M158', 'M159', 'M160', 'M161', 'M162', 'M163', 'M164', 'M165', 'M166', 'M167', 'M168', 'M169', 'M170', 'M171', 'M172', 'M173', 'M174', 'M175', 'M176', 'M177', 'M178', 'M179', 'M180', 'M181', 'M182', 'M183', 'M184', 'M185', 'M186', 'M187', 'M188', 'M189', 'M190', 'M191', 'M192', 'M193', 'M194', 'M195', 'M196', 'M197', 'M198', 'M199', 'M200'], acknowledged=True)
# 2) Log transaksi (berkas teks, 400 ribu baris)
#    format: waktu|id_trx|id_user|id_merchant|nominal|status
n = 400_000
waktu = pd.Timestamp("2026-09-01") + pd.to_timedelta(rng.integers(0, 30 * 86400, n), unit="s")
id_m = rng.choice([m["_id"] for m in merchant], n)
nominal = np.round(rng.lognormal(10.6, 0.9, n), -2).astype(int)
status = rng.choice(["SUKSES", "GAGAL", "PENDING"], n, p=[0.93, 0.05, 0.02])

# Menyuntikkan kesalahan sistem (-1) dan nilai ekstrem mencurigakan.
nominal[rng.choice(n, 150, replace=False)] = -1
nominal[rng.choice(n, 80, replace=False)] = 250_000_000

# Menyusun baris log berpemisah "|".
baris = [f"{w:%Y-%m-%d %H:%M:%S}|T{i:07d}|U{u:05d}|{m}|{v}|{s}"
         for i, (w, u, m, v, s) in enumerate(
             zip(waktu, rng.integers(1, 50_000, n), id_m, nominal, status))]

# Setiap baris ke-5.000 diganti baris log rusak.
baris[::5000] = ["ERROR koneksi timeout"] * len(baris[::5000])
jalur_log = os.path.join(folder, "transaksi.log")
with open(jalur_log, "w") as f:
    f.write("\n".join(baris))
## 21622886
# 3) Landing zone untuk streaming (3 berkas JSON, 3% nominal null)
landing = os.path.join(folder, "landing"); os.makedirs(landing)
for b in range(3):
    with open(os.path.join(landing, f"batch_{b}.json"), "w") as f:
        for k in range(500):
            e = {"id_trx": f"S{b}{k:04d}", "id_merchant": str(rng.choice(id_m)),
                 "nominal": int(rng.lognormal(10.6, 0.9)) if rng.random() > 0.03 else None,
                 "waktu": str(pd.Timestamp("2026-10-01 09:00")
                              + pd.Timedelta(minutes=b * 5 + k % 5))}
            f.write(json.dumps(e) + "\n")
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 93
## 93
## 94
## 94
## 95
## 94
## 95
## 94
## 93
## 94
## 94
## 95
## 94
## 95
## 93
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 93
## 93
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 93
## 95
## 95
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 95
## 94
## 94
## 93
## 94
## 93
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 95
## 93
## 94
## 94
## 95
## 94
## 95
## 93
## 94
## 95
## 94
## 94
## 95
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 95
## 95
## 95
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 95
## 94
## 95
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 93
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 93
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 93
## 95
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 95
## 94
## 93
## 93
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 95
## 95
## 95
## 94
## 94
## 94
## 94
## 96
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 93
## 94
## 93
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 93
## 94
## 94
## 95
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 93
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 93
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 95
## 95
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 93
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 93
## 94
## 93
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 94
## 93
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 93
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 95
## 94
## 95
## 94
## 95
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 93
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 93
## 95
## 95
## 94
## 95
## 94
## 95
## 93
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 93
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 95
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 93
## 95
## 93
## 94
## 95
## 94
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 93
## 94
## 95
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 94
## 93
## 94
## 94
## 95
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 93
## 94
## 94
## 95
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 95
## 96
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 93
## 95
## 95
## 94
## 94
## 93
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 93
## 93
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 93
## 94
## 93
## 94
## 95
## 95
## 94
## 93
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 93
## 94
## 94
## 94
## 93
## 93
## 95
## 94
## 94
## 95
## 94
## 94
## 93
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 95
## 95
## 93
## 93
## 94
## 94
## 94
## 95
## 95
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 93
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 95
## 93
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 93
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 95
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 95
## 93
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 93
## 94
## 95
## 94
## 94
## 94
## 94
## 95
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 93
## 94
## 95
## 94
## 95
## 93
## 93
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 94
## 94
## 93
## 94
## 93
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 95
## 94
## 94
## 94
## 93
## 95
## 94
## 94
## 95
## 94
## 94
## 94
## 93
# Ringkasan dataset ujian.
print("Merchant:", db["merchant"].count_documents({}),
      "| baris log:", len(baris),
      "| berkas landing:", len(os.listdir(landing)))
## Merchant: 200 | baris log: 400000 | berkas landing: 3

Interpretasi:

Berdasarkan hasil tersebut, proses pembuatan data berhasil dilakukan dengan menghasilkan 200 data merchant, 400.000 baris transaksi, dan 3 berkas data tambahan. Seluruh data merchant berhasil dimasukkan ke dalam basis data, ditandai dengan status acknowledged=True. Dengan demikian, data yang dibutuhkan telah berhasil disiapkan dan siap digunakan untuk proses pengolahan serta analisis selanjutnya.

Tugas C1: Kueri dan Agregasi NoSQL

  1. Dengan MongoDB (db["merchant"]), tampilkan nama merchant Kuliner di Bandung yang memiliki promo "cashback" dan rating \(\ge\) 4,0. (b) Hitung jumlah merchant yang memiliki field jam_buka. (c) Dengan aggregation pipeline, hitung jumlah merchant dan rata-rata rating per kategori, urutkan dari rata-rata rating tertinggi.
# ==============================================================================
# C1.1 Merchant Kuliner di Bandung
# Promo "cashback" dan Rating >= 4.0
# ==============================================================================

merchant_kuliner = db["merchant"].find(
    {
        "kategori": "Kuliner",
        "kota": "Bandung",
        "promo": "cashback",
        "rating": {"$gte": 4.0}
    },
    {
        "_id": 0,
        "nama": 1,
        "rating": 1,
        "promo": 1
    }
)

print("=== C1.1 Merchant Kuliner Cashback di Bandung (Rating >= 4.0) ===")

for m in merchant_kuliner:
    print(m)


# ==============================================================================
# C1.2 Jumlah Merchant yang Memiliki Field jam_buka
# ==============================================================================

total_jam_buka = db["merchant"].count_documents(
    {"jam_buka": {"$exists": True}}
)

print(
    f"\n=== C1.2 Total Merchant dengan Atribut jam_buka: "
    f"{total_jam_buka} ==="
)


# ==============================================================================
# C1.3 Aggregation Pipeline
# Jumlah Merchant dan Rata-rata Rating per Kategori
# ==============================================================================

pipeline = [
    {
        "$group": {
            "_id": "$kategori",
            "jumlah_merchant": {"$sum": 1},
            "rata_rating": {"$avg": "$rating"}
        }
    },
    {
        "$sort": {
            "rata_rating": -1
        }
    }
]

agregasi_kategori = list(
    db["merchant"].aggregate(pipeline)
)

print("\n=== C1.3 Agregasi Merchant per Kategori ===")

for row in agregasi_kategori:
    print(
        f"Kategori: {row['_id']:<15} | "
        f"Jumlah: {row['jumlah_merchant']:<3} | "
        f"Rata-rata Rating: {row['rata_rating']:.2f}"
    )

Interpretasi:

Berdasarkan hasil pengolahan data:

  1. C1.1 Merchant Kuliner Cashback di Bandung Ditemukan satu merchant yang memenuhi kriteria, yaitu Merchant 75 dengan rating 4,1 dan memiliki promo cashback.

  2. C1.2 Merchant dengan Atribut jam_buka Terdapat 61 merchant yang memiliki atribut jam_buka dalam data.

  3. C1.3 Agregasi Merchant per Kategori Kategori Transportasi memiliki jumlah merchant terbanyak, yaitu 48 merchant, sedangkan Kuliner memiliki rata-rata rating tertinggi sebesar 4,15. Kategori Retail memiliki rata-rata rating terendah, yaitu 3,90.

Kesimpulan: Data menunjukkan bahwa kategori Transportasi mendominasi jumlah merchant, sementara kategori Kuliner memperoleh penilaian rata-rata tertinggi dari pengguna.

Tugas C2: Pemrosesan Log dengan Spark RDD

  1. Baca jalur_log sebagai RDD, urai setiap baris menjadi tuple (waktu, id_trx, id_user, id_merchant, nominal, status), buang baris rusak, dan hitung jumlah baris rusak dengan accumulator. (b) Hitung jumlah transaksi per status dengan reduceByKey. (c) Hitung tingkat kegagalan (persentase status GAGAL) per id_merchant, lalu tampilkan 5 merchant dengan tingkat kegagalan tertinggi (minimal 1.500 transaksi).
# ==============================================================================
# C2.1 Membaca dan Membersihkan Data Menggunakan Spark RDD
# ==============================================================================

# Membuat accumulator untuk menghitung jumlah baris rusak
accumulator_rusak = sc.accumulator(0)

# Fungsi untuk mengurai dan memvalidasi setiap baris log
def parse_log_line(line):
    parts = line.strip().split("|")

    if len(parts) == 6:
        try:
            waktu = parts[0].strip()
            id_trx = parts[1].strip()
            id_user = parts[2].strip()
            id_merchant = parts[3].strip()
            nominal = int(parts[4].strip())
            status = parts[5].strip()

            if not all([waktu, id_trx, id_user, id_merchant, status]):
                raise ValueError("Kolom kosong")

            return [
                (waktu, id_trx, id_user, id_merchant, nominal, status)
            ]

        except ValueError:
            pass

    # Menambah accumulator untuk setiap baris yang tidak valid
    accumulator_rusak.add(1)
    return []

# Membaca file log sebagai RDD dan membersihkan data
log_rdd = (
    sc.textFile(jalur_log)
    .flatMap(parse_log_line)
    .cache()
)

# Menghitung jumlah data valid
total_valid = log_rdd.count()
total_rusak = accumulator_rusak.value

# Menampilkan hasil pemeriksaan
print("=== C2.1 PEMERIKSAAN DATA RDD ===")
## === C2.1 PEMERIKSAAN DATA RDD ===
print(f"Jumlah Baris Valid : {total_valid:,}")
## Jumlah Baris Valid : 399,920
print(f"Jumlah Baris Rusak : {total_rusak:,}")
## Jumlah Baris Rusak : 80
# Menampilkan 5 contoh data valid langsung dari file,
# tanpa menjalankan aksi RDD take()
contoh_data = []

with open(jalur_log, "r", encoding="utf-8") as file:
    for line in file:
        parts = line.strip().split("|")

        if len(parts) != 6:
            continue

        try:
            row = (
                parts[0].strip(),
                parts[1].strip(),
                parts[2].strip(),
                parts[3].strip(),
                int(parts[4].strip()),
                parts[5].strip()
            )

            if not all([row[0], row[1], row[2], row[3], row[5]]):
                continue

            contoh_data.append(row)

            if len(contoh_data) == 5:
                break

        except ValueError:
            continue

print("\n=== 5 CONTOH DATA VALID ===")
## 
## === 5 CONTOH DATA VALID ===
for row in contoh_data:
    print(row)
## ('2026-09-20 01:13:10', 'T0000001', 'U42902', 'M172', 25000, 'SUKSES')
## ('2026-09-29 10:06:45', 'T0000002', 'U03879', 'M041', 196700, 'SUKSES')
## ('2026-09-19 20:12:13', 'T0000003', 'U29593', 'M094', 7500, 'SUKSES')
## ('2026-09-10 10:43:30', 'T0000004', 'U47702', 'M002', 77800, 'SUKSES')
## ('2026-09-04 10:00:00', 'T0000005', 'U16643', 'M175', 27200, 'SUKSES')

Interpretasi:

Berdasarkan hasil pemeriksaan, terdapat 399.920 baris data valid dan 80 baris data rusak dari keseluruhan 400.000 baris transaksi. Lima contoh data valid menunjukkan bahwa setiap baris berhasil dibaca dan dipisahkan menjadi enam atribut, yaitu waktu transaksi, ID transaksi, ID pengguna, ID merchant, nominal, dan status.

Kesimpulan: Proses pembacaan dan pemisahan data RDD berhasil dilakukan. Sebanyak 80 baris yang tidak sesuai format berhasil teridentifikasi sebagai data rusak, sedangkan 399.920 baris lainnya siap digunakan untuk proses analisis berikutnya.


# ==============================================================================
# C2.2 JUMLAH TRANSAKSI PER STATUS (reduceByKey)
# ==============================================================================

# 1. Memastikan RDD C2.1 tersedia
if "log_rdd" not in globals():
    raise ValueError(
        "log_rdd belum tersedia. Jalankan C2.1 terlebih dahulu."
    )

# 2. Membentuk pasangan (status, 1)
status_rdd = log_rdd.map(
    lambda r: (r[5], 1)
)

# 3. Menghitung jumlah transaksi menggunakan reduceByKey
status_counts_rdd = status_rdd.reduceByKey(
    lambda a, b: a + b
)

# 4. Mengambil dan mengurutkan hasil
status_counts = sorted(
    status_counts_rdd.collect(),
    key=lambda x: x[0]
)

# 5. Menampilkan hasil
print("=== C2.2 JUMLAH TRANSAKSI PER STATUS ===")
## === C2.2 JUMLAH TRANSAKSI PER STATUS ===
for status, jumlah in status_counts:
    print(f"Status: {status:<10} | Jumlah: {jumlah:,}")
## Status: GAGAL      | Jumlah: 20,166
## Status: PENDING    | Jumlah: 7,884
## Status: SUKSES     | Jumlah: 371,870
# 6. Validasi total transaksi
total_transaksi = sum(
    jumlah for status, jumlah in status_counts
)

print(f"\nTotal Transaksi Valid: {total_transaksi:,}")
## 
## Total Transaksi Valid: 399,920

Interpretasi:

Berdasarkan hasil pengelompokan data, terdapat 371.870 transaksi berstatus SUKSES, 20.166 transaksi berstatus GAGAL, dan 7.884 transaksi berstatus PENDING. Total seluruh transaksi valid adalah 399.920, sesuai dengan hasil pemeriksaan data pada C2.1.

Kesimpulan: Transaksi berstatus SUKSES mendominasi keseluruhan data. Sementara itu, transaksi GAGAL dan PENDING memiliki jumlah yang jauh lebih sedikit. Hasil ini menunjukkan bahwa sebagian besar transaksi berhasil diproses.


# ==============================================================================
# C2.3 Top 5 Merchant dengan Tingkat Kegagalan Tertinggi
# Minimal 1.500 Transaksi
# ==============================================================================

# Memastikan RDD dari C2.1 sudah tersedia
if "log_rdd" not in globals():
    raise ValueError("log_rdd belum tersedia. Jalankan C2.1 terlebih dahulu.")

# Membentuk pasangan:
# (id_merchant, (jumlah_gagal, total_transaksi))
merchant_failure = (
    log_rdd
    .map(
        lambda r: (
            r[3],
            (1 if r[5] == "GAGAL" else 0, 1)
        )
    )

    # Menghitung jumlah gagal dan total transaksi per merchant
    .reduceByKey(
        lambda a, b: (
            a[0] + b[0],
            a[1] + b[1]
        )
    )

    # Memilih merchant dengan minimal 1.500 transaksi
    .filter(lambda kv: kv[1][1] >= 1500)

    # Menghitung persentase transaksi gagal
    .mapValues(
        lambda v: round((v[0] / v[1]) * 100, 2)
    )
)

# Mengambil 5 merchant dengan tingkat kegagalan tertinggi
top_failing_merchants = merchant_failure.takeOrdered(
    5,
    key=lambda kv: -kv[1]
)

# Menampilkan hasil
print("=== C2.3 TOP 5 MERCHANT TINGKAT KEGAGALAN TERTINGGI ===")
## === C2.3 TOP 5 MERCHANT TINGKAT KEGAGALAN TERTINGGI ===
print("Syarat: Minimal 1.500 transaksi\n")
## Syarat: Minimal 1.500 transaksi
for merchant_id, failure_rate in top_failing_merchants:
    print(
        f"ID Merchant: {merchant_id} | "
        f"Persentase GAGAL: {failure_rate:.2f}%"
    )
## ID Merchant: M164 | Persentase GAGAL: 6.34%
## ID Merchant: M048 | Persentase GAGAL: 6.17%
## ID Merchant: M186 | Persentase GAGAL: 6.16%
## ID Merchant: M147 | Persentase GAGAL: 6.13%
## ID Merchant: M145 | Persentase GAGAL: 6.01%

Interpretasi:

Berdasarkan hasil analisis terhadap merchant yang memiliki minimal 1.500 transaksi, diperoleh lima merchant dengan persentase kegagalan tertinggi. Merchant M164 menempati urutan pertama dengan tingkat kegagalan sebesar 6,34%, diikuti M048 sebesar 6,17%, M186 sebesar 6,16%, M147 sebesar 6,13%, dan M145 sebesar 6,01%.

Kesimpulan: Merchant M164 memiliki tingkat kegagalan transaksi tertinggi di antara lima merchant yang dianalisis. Hasil ini dapat menjadi dasar untuk melakukan pemeriksaan lebih lanjut terhadap penyebab kegagalan transaksi pada merchant tersebut.


Tugas C3: Analisis dengan Spark DataFrame dan SQL

  1. Ubah RDD hasil C2 menjadi DataFrame dengan skema yang benar (waktu bertipe timestamp, nominal bertipe long). (b) Buat DataFrame merchant dari MongoDB (hanya _id, kategori, kota) dan gabungkan dengan transaksi SUKSES yang nominalnya valid (0 < nominal < 50 juta) menggunakan broadcast join. (c) Hitung total nominal per kota per kategori. (d) Dengan window function atau Spark SQL, tampilkan kategori teratas di setiap kota beserta porsinya (%) terhadap total kota tersebut.
# ==============================================================================
# C3.1 KONVERSI RDD KE DATAFRAME DENGAN SKEMA EKSPLISIT
# ==============================================================================

from datetime import datetime

from pyspark.sql import functions as F
from pyspark.sql.types import (
    StructType,
    StructField,
    StringType,
    LongType,
    TimestampType
)
from pyspark.sql.window import Window


# Mendefinisikan skema transaksi
skema_transaksi = StructType([
    StructField("waktu", TimestampType(), True),
    StructField("id_trx", StringType(), True),
    StructField("id_user", StringType(), True),
    StructField("id_merchant", StringType(), True),
    StructField("nominal", LongType(), True),
    StructField("status", StringType(), True)
])


# Mengubah teks waktu menjadi objek datetime
rdd_transaksi_datetime = log_rdd.map(
    lambda r: (
        datetime.strptime(r[0], "%Y-%m-%d %H:%M:%S"),
        r[1],
        r[2],
        r[3],
        r[4],
        r[5]
    )
)


# Membuat DataFrame transaksi
df_trx_raw = spark.createDataFrame(
    rdd_transaksi_datetime,
    schema=skema_transaksi
)


print("=== C3.1 SKEMA DATAFRAME TRANSAKSI ===")
## === C3.1 SKEMA DATAFRAME TRANSAKSI ===
df_trx_raw.printSchema()
## root
##  |-- waktu: timestamp (nullable = true)
##  |-- id_trx: string (nullable = true)
##  |-- id_user: string (nullable = true)
##  |-- id_merchant: string (nullable = true)
##  |-- nominal: long (nullable = true)
##  |-- status: string (nullable = true)
print("Contoh Data Transaksi:")
## Contoh Data Transaksi:
df_trx_raw.show(5, truncate=False)
## +-------------------+--------+-------+-----------+-------+------+
## |waktu              |id_trx  |id_user|id_merchant|nominal|status|
## +-------------------+--------+-------+-----------+-------+------+
## |2026-09-20 01:13:10|T0000001|U42902 |M172       |25000  |SUKSES|
## |2026-09-29 10:06:45|T0000002|U03879 |M041       |196700 |SUKSES|
## |2026-09-19 20:12:13|T0000003|U29593 |M094       |7500   |SUKSES|
## |2026-09-10 10:43:30|T0000004|U47702 |M002       |77800  |SUKSES|
## |2026-09-04 10:00:00|T0000005|U16643 |M175       |27200  |SUKSES|
## +-------------------+--------+-------+-----------+-------+------+
## only showing top 5 rows

Interpretasi:

Berdasarkan hasil pemeriksaan, data transaksi berhasil diubah dari RDD menjadi DataFrame dengan enam kolom, yaitu waktu, id_trx, id_user, id_merchant, nominal, dan status.

Tipe data setiap kolom telah sesuai dengan ketentuan: waktu bertipe timestamp, nominal bertipe long, sedangkan kolom lainnya bertipe string. Lima contoh data juga menunjukkan bahwa nilai transaksi berhasil terbaca dengan benar.

# ==============================================================================
# C3.2 CLEANING DATA DAN BROADCAST JOIN DENGAN DATA MERCHANT
# ==============================================================================

# Memeriksa ketersediaan koneksi MongoDB
if "db" not in globals():
    raise RuntimeError(
        "Koneksi MongoDB belum tersedia. "
        "Jalankan kode koneksi MongoDB pada bagian C1 terlebih dahulu."
    )


# Memfilter transaksi SUKSES dengan nominal valid
df_trx_sukses = df_trx_raw.filter(
    (F.col("status") == "SUKSES") &
    (F.col("nominal") > 0) &
    (F.col("nominal") < 50000000)
)


# Mengambil data merchant dari MongoDB
merchant_docs = list(
    db["merchant"].find(
        {},
        {
            "_id": 1,
            "kategori": 1,
            "kota": 1
        }
    )
)


# Memeriksa apakah data merchant tersedia
if not merchant_docs:
    raise ValueError(
        "Data merchant kosong. Periksa database dan collection merchant."
    )


print("Jumlah data merchant:", len(merchant_docs))
## Jumlah data merchant: 200
# Membuat skema DataFrame merchant
skema_merchant = StructType([
    StructField("_id", StringType(), True),
    StructField("kategori", StringType(), True),
    StructField("kota", StringType(), True)
])


# Mengubah dokumen MongoDB menjadi tuple
data_merchant = [
    (
        str(doc["_id"]),
        doc.get("kategori"),
        doc.get("kota")
    )
    for doc in merchant_docs
]


# Membuat DataFrame merchant
df_merchant = spark.createDataFrame(
    data_merchant,
    schema=skema_merchant
)


# Melakukan Broadcast Join
df_joined = df_trx_sukses.join(
    F.broadcast(df_merchant),
    df_trx_sukses.id_merchant == df_merchant["_id"],
    how="inner"
)


print("\n=== C3.2 HASIL BROADCAST JOIN ===")
## 
## === C3.2 HASIL BROADCAST JOIN ===
print("Jumlah transaksi SUKSES valid:", df_trx_sukses.count())
## Jumlah transaksi SUKSES valid: 371658
print("Jumlah data merchant:", df_merchant.count())
## Jumlah data merchant: 200
print("Jumlah data hasil join:", df_joined.count())
## Jumlah data hasil join: 371658
df_joined.select(
    df_trx_sukses["id_trx"],
    df_trx_sukses["id_merchant"],
    df_trx_sukses["nominal"],
    df_merchant["kategori"],
    df_merchant["kota"]
).show(5, truncate=False)
## +--------+-----------+-------+------------+--------+
## |id_trx  |id_merchant|nominal|kategori    |kota    |
## +--------+-----------+-------+------------+--------+
## |T0000001|M172       |25000  |Hiburan     |Medan   |
## |T0000002|M041       |196700 |Transportasi|Jakarta |
## |T0000003|M094       |7500   |Retail      |Makassar|
## |T0000004|M002       |77800  |Hiburan     |Bandung |
## |T0000005|M175       |27200  |Kuliner     |Medan   |
## +--------+-----------+-------+------------+--------+
## only showing top 5 rows

Interpretasi:

Berdasarkan hasil pengolahan, terdapat 200 data merchant yang berhasil digunakan dalam proses penggabungan dengan data transaksi. Dari 399.920 transaksi valid, sebanyak 371.658 transaksi SUKSES dengan nominal valid memenuhi kriteria penyaringan.

Setelah dilakukan broadcast join, jumlah data hasil penggabungan tetap 371.658 baris. Hasil ini menunjukkan bahwa transaksi berhasil dipasangkan dengan informasi merchant, yaitu kategori dan kota, tanpa mengurangi jumlah transaksi yang memenuhi kriteria.

# ==============================================================================
# C3.3 TOTAL NOMINAL PER KOTA PER KATEGORI
# ==============================================================================

omzet_kota_kategori = (
    df_joined
    .groupBy("kota", "kategori")
    .agg(
        F.sum("nominal").alias("total_nominal")
    )
)


print("\n=== C3.3 TOTAL NOMINAL PER KOTA DAN KATEGORI ===")
## 
## === C3.3 TOTAL NOMINAL PER KOTA DAN KATEGORI ===
omzet_kota_kategori.orderBy(
    "kota",
    F.desc("total_nominal")
).show(25, truncate=False)
## +--------+------------+-------------+
## |kota    |kategori    |total_nominal|
## +--------+------------+-------------+
## |Bandung |Transportasi|1455560000   |
## |Bandung |Hiburan     |1100050900   |
## |Bandung |Retail      |1016883400   |
## |Bandung |Kuliner     |898654300    |
## |Bandung |Kesehatan   |668446100    |
## |Jakarta |Transportasi|1138838700   |
## |Jakarta |Hiburan     |888514200    |
## |Jakarta |Kesehatan   |778065700    |
## |Jakarta |Retail      |556442400    |
## |Jakarta |Kuliner     |214176200    |
## |Makassar|Retail      |1344025800   |
## |Makassar|Kesehatan   |1236338300   |
## |Makassar|Hiburan     |1003119800   |
## |Makassar|Kuliner     |580311300    |
## |Makassar|Transportasi|557000900    |
## |Medan   |Transportasi|1439104900   |
## |Medan   |Kesehatan   |1008722600   |
## |Medan   |Hiburan     |1005036300   |
## |Medan   |Kuliner     |805227200    |
## |Medan   |Retail      |776654600    |
## |Surabaya|Kesehatan   |1000164700   |
## |Surabaya|Hiburan     |868802300    |
## |Surabaya|Transportasi|800425300    |
## |Surabaya|Kuliner     |677970800    |
## |Surabaya|Retail      |564119200    |
## +--------+------------+-------------+

Interpretasi:

Berdasarkan hasil pengelompokan data, diperoleh total nominal transaksi dari lima kategori merchant pada masing-masing kota.

Beberapa temuan utama dari hasil tersebut: 1. Bandung: Kategori Transportasi memiliki total nominal tertinggi sebesar Rp1.455.560.000. 2. Jakarta: Kategori Transportasi mendominasi dengan total Rp1.138.838.700. 3. Makassar: Kategori Retail memiliki total tertinggi sebesar Rp1.344.025.800. 4. Medan: Kategori Transportasi menempati posisi tertinggi sebesar Rp1.439.104.900. 5. Surabaya: Kategori Kesehatan memiliki total tertinggi sebesar Rp1.000.164.700.

# ==============================================================================
# C3.4 KATEGORI TERATAS PER KOTA DAN PORSI PERSENTASE
# MENGGUNAKAN WINDOW FUNCTION
# ==============================================================================

# Window untuk menghitung total nominal seluruh kategori per kota
w_total = Window.partitionBy("kota")


# Window untuk menentukan peringkat kategori berdasarkan nominal
w_rank = Window.partitionBy("kota").orderBy(
    F.desc("total_nominal")
)


df_kategori_teratas = (
    omzet_kota_kategori

    # Menghitung total nominal seluruh kategori dalam setiap kota
    .withColumn(
        "total_kota",
        F.sum("total_nominal").over(w_total)
    )

    # Menghitung porsi nominal kategori terhadap total kota
    .withColumn(
        "porsi_persen",
        F.round(
            100.0 * F.col("total_nominal") / F.col("total_kota"),
            2
        )
    )

    # Memberikan peringkat berdasarkan total nominal
    .withColumn(
        "peringkat",
        F.rank().over(w_rank)
    )

    # Mengambil kategori dengan peringkat tertinggi
    .filter(F.col("peringkat") == 1)

    # Memilih kolom hasil
    .select(
        "kota",
        "kategori",
        "total_nominal",
        "porsi_persen"
    )

    .orderBy("kota")
)


print("\n=== C3.4 KATEGORI TERATAS DAN PORSI (%) PER KOTA ===")
## 
## === C3.4 KATEGORI TERATAS DAN PORSI (%) PER KOTA ===
df_kategori_teratas.show(truncate=False)
## +--------+------------+-------------+------------+
## |kota    |kategori    |total_nominal|porsi_persen|
## +--------+------------+-------------+------------+
## |Bandung |Transportasi|1455560000   |28.32       |
## |Jakarta |Transportasi|1138838700   |31.85       |
## |Makassar|Retail      |1344025800   |28.47       |
## |Medan   |Transportasi|1439104900   |28.58       |
## |Surabaya|Kesehatan   |1000164700   |25.57       |
## +--------+------------+-------------+------------+

Interpretasi:

Berdasarkan hasil analisis, setiap kota memiliki kategori merchant dengan total nominal transaksi tertinggi yang berbeda. Bandung, Jakarta, dan Medan didominasi kategori Transportasi, dengan porsi masing-masing 28,32%, 31,85%, dan 28,58%. Sementara itu, Makassar didominasi kategori Retail sebesar 28,47%, sedangkan Surabaya didominasi kategori Kesehatan sebesar 25,57%.

Tugas C4: Ingestion dengan Structured Streaming

Bangun kueri streaming dari folder landing (format JSON, skema eksplisit) dengan trigger(availableNow=True), maxFilesPerTrigger=1, checkpoint, dan foreachBatch yang: (a) memisahkan event dengan nominal null sebagai tidak valid, (b) menyimpan event valid ke Parquet, dan (c) mencatat jumlah event valid dan tidak valid per batch. Tampilkan catatan per batch dan total baris Parquet.

# ==============================================================================
# C4: STRUCTURED STREAMING - PEMROSESAN DATA JSON
# ==============================================================================

import os
import shutil
from pyspark.sql.types import (
    StructType, StructField,
    StringType, TimestampType, LongType
)
from pyspark.sql import functions as F

# ------------------------------------------------------------------------------
# 1. Menentukan direktori landing
# ------------------------------------------------------------------------------

# Memastikan jalur_log sudah tersedia
if "jalur_log" not in globals():
    raise ValueError(
        "jalur_log belum didefinisikan. "
        "Pastikan chunk pembacaan data transaksi dijalankan terlebih dahulu."
    )

# Menentukan folder berdasarkan lokasi file transaksi
base_dir = os.path.dirname(jalur_log)
path_landing = os.path.join(base_dir, "landing")

print("=== PEMERIKSAAN DIREKTORI ===")
## === PEMERIKSAAN DIREKTORI ===
print("File transaksi :", jalur_log)
## File transaksi : C:\Users\DWIMAN~1\AppData\Local\Temp\tmpqkcz2kgv\transaksi.log
print("Folder landing :", path_landing)
## Folder landing : C:\Users\DWIMAN~1\AppData\Local\Temp\tmpqkcz2kgv\landing
# Memastikan folder landing tersedia
if not os.path.isdir(path_landing):
    raise FileNotFoundError(
        f"Folder landing tidak ditemukan: {path_landing}"
    )

# Memeriksa file JSON
file_json = sorted([
    f for f in os.listdir(path_landing)
    if f.lower().endswith(".json")
])

print("Jumlah file JSON:", len(file_json))
## Jumlah file JSON: 3
for nama_file in file_json:
    print("-", nama_file)
## - batch_0.json
## - batch_1.json
## - batch_2.json
if len(file_json) == 0:
    raise FileNotFoundError(
        "Tidak ditemukan file JSON di folder landing."
    )

# ------------------------------------------------------------------------------
# 2. Menyiapkan direktori checkpoint dan Parquet
# ------------------------------------------------------------------------------

path_checkpoint_final = os.path.join(
    base_dir, "checkpoint_c4_final"
)

path_parquet_final = os.path.join(
    base_dir, "parquet_c4_final"
)

# Menghapus hasil percobaan sebelumnya
for folder in [path_checkpoint_final, path_parquet_final]:
    if os.path.exists(folder):
        shutil.rmtree(folder)

os.makedirs(path_checkpoint_final, exist_ok=True)

# ------------------------------------------------------------------------------
# 3. Membuat skema JSON
# ------------------------------------------------------------------------------

schema_streaming = StructType([
    StructField("waktu", TimestampType(), True),
    StructField("id_trx", StringType(), True),
    StructField("id_user", StringType(), True),
    StructField("id_merchant", StringType(), True),
    StructField("nominal", LongType(), True),
    StructField("status", StringType(), True)
])

# ------------------------------------------------------------------------------
# 4. Membaca file JSON sebagai Streaming DataFrame
# ------------------------------------------------------------------------------

stream_df = (
    spark.readStream
    .schema(schema_streaming)
    .option("maxFilesPerTrigger", 1)
    .json(path_landing)
)

# ------------------------------------------------------------------------------
# 5. Menyiapkan catatan setiap batch
# ------------------------------------------------------------------------------

catatan_streaming = []

# ------------------------------------------------------------------------------
# 6. Fungsi pemrosesan setiap batch
# ------------------------------------------------------------------------------

def proses_batch(batch_df, batch_id):

    # Memisahkan data valid dan invalid berdasarkan nominal
    df_valid = batch_df.filter(
        F.col("nominal").isNotNull()
    )

    df_invalid = batch_df.filter(
        F.col("nominal").isNull()
    )

    # Menghitung jumlah data
    jumlah_valid = df_valid.count()
    jumlah_invalid = df_invalid.count()

    # Menyimpan data valid ke Parquet
    if jumlah_valid > 0:
        (
            df_valid.write
            .mode("append")
            .parquet(path_parquet_final)
        )

    # Mencatat hasil setiap batch
    catatan_streaming.append({
        "batch_id": batch_id,
        "jumlah_valid": jumlah_valid,
        "jumlah_invalid": jumlah_invalid
    })

# ------------------------------------------------------------------------------
# 7. Menjalankan Structured Streaming
# ------------------------------------------------------------------------------

query_stream = (
    stream_df.writeStream
    .foreachBatch(proses_batch)
    .option("checkpointLocation", path_checkpoint_final)
    .trigger(availableNow=True)
    .start()
)

query_stream.awaitTermination()

print("\nStructured Streaming selesai diproses.")
## 
## Structured Streaming selesai diproses.

Interpretasi:

Berdasarkan hasil pemeriksaan, file transaksi berhasil ditemukan dan folder landing terdeteksi dengan benar. Di dalam folder tersebut terdapat 3 file JSON, yaitu batch_0.json, batch_1.json, dan batch_2.json.

Proses Structured Streaming juga berhasil diselesaikan tanpa menampilkan pesan kesalahan.

# ==============================================================================
# C4: MENAMPILKAN LOG PER BATCH DAN TOTAL DATA PARQUET
# ==============================================================================

print("=== LOG PER BATCH ===")
## === LOG PER BATCH ===
for catatan in catatan_streaming:
    print(
        f"Batch {catatan['batch_id']} | "
        f"Valid: {catatan['jumlah_valid']} | "
        f"Invalid: {catatan['jumlah_invalid']}"
    )
## Batch 0 | Valid: 486 | Invalid: 14
## Batch 1 | Valid: 479 | Invalid: 21
## Batch 2 | Valid: 482 | Invalid: 18
# Menghitung total valid dan invalid
total_valid_stream = sum(
    x["jumlah_valid"] for x in catatan_streaming
)

total_invalid_stream = sum(
    x["jumlah_invalid"] for x in catatan_streaming
)

print("\n=== RINGKASAN STREAMING ===")
## 
## === RINGKASAN STREAMING ===
print("Total valid   :", total_valid_stream)
## Total valid   : 1447
print("Total invalid :", total_invalid_stream)
## Total invalid : 53
print("Total diproses:", total_valid_stream + total_invalid_stream)
## Total diproses: 1500
print("\n=== TOTAL DATA PARQUET ===")
## 
## === TOTAL DATA PARQUET ===
if os.path.exists(path_parquet_final):

    df_hasil = spark.read.parquet(path_parquet_final)

    print("Total baris Parquet:", df_hasil.count())

    print("\n=== SKEMA DATA PARQUET ===")
    df_hasil.printSchema()

else:
    print("Folder output Parquet belum terbentuk.")
## Total baris Parquet: 1447
## 
## === SKEMA DATA PARQUET ===
## root
##  |-- waktu: timestamp (nullable = true)
##  |-- id_trx: string (nullable = true)
##  |-- id_user: string (nullable = true)
##  |-- id_merchant: string (nullable = true)
##  |-- nominal: long (nullable = true)
##  |-- status: string (nullable = true)

Interpretasi:

Berdasarkan hasil pemrosesan tiga batch data, diperoleh rincian sebagai berikut: 1. Batch 0: 486 data valid dan 14 data tidak valid. 2. Batch 1: 479 data valid dan 21 data tidak valid. 3. Batch 2: 482 data valid dan 18 data tidak valid. Secara keseluruhan, dari 1.500 data yang diproses, terdapat 1.447 data valid dan 53 data tidak valid. Seluruh 1.447 data valid berhasil disimpan ke dalam format Parquet. Jumlah baris pada file Parquet sesuai dengan total data valid hasil streaming.

Skema data Parquet juga telah sesuai, yaitu waktu bertipe timestamp, nominal bertipe long, serta kolom lainnya bertipe string.

Tugas C5: EDA Skala Besar dan Insight

Gunakan DataFrame transaksi dari C3(a). (a) Buat laporan kualitas data dalam satu select: jumlah nominal negatif, nominal \(\ge\) 50 juta, dan status PENDING. (b) Hitung persentil 5, 50, 95, dan 99 nominal transaksi valid dengan approxQuantile. (c) Hitung jumlah transaksi SUKSES per jam (0–23) di Spark dan buat grafik batangnya. (d) Tuliskan dua insight (temuan, bukti, tindakan).

from pyspark.sql import functions as F

# ==========================================
# C5(a) LAPORAN KUALITAS DATA
# ==========================================

laporan_kualitas = df_trx_raw.select(
    F.sum(
        F.when(F.col("nominal") < 0, 1).otherwise(0)
    ).alias("nominal_negatif"),

    F.sum(
        F.when(F.col("nominal") >= 50000000, 1).otherwise(0)
    ).alias("nominal_minimal_50_juta"),

    F.sum(
        F.when(F.col("status") == "PENDING", 1).otherwise(0)
    ).alias("status_pending")
)

# Menampilkan laporan
print("=== LAPORAN KUALITAS DATA ===")
## === LAPORAN KUALITAS DATA ===
laporan_kualitas.show(truncate=False)
## +---------------+-----------------------+--------------+
## |nominal_negatif|nominal_minimal_50_juta|status_pending|
## +---------------+-----------------------+--------------+
## |150            |80                     |7884          |
## +---------------+-----------------------+--------------+

Interpretasi:

Berdasarkan hasil pemeriksaan kualitas data, ditemukan 150 transaksi dengan nominal negatif, 80 transaksi dengan nominal minimal Rp50 juta, dan 7.884 transaksi berstatus PENDING.

Ketiga kondisi tersebut menunjukkan adanya data yang perlu mendapat perhatian dalam proses pengolahan transaksi. Nominal negatif dan nominal yang terlalu besar perlu diperiksa kembali, sedangkan transaksi PENDING perlu dipantau karena belum berstatus berhasil maupun gagal.

# Mengambil transaksi dengan nominal valid
df_nominal_valid = df_trx_raw.filter(
    (F.col("nominal") > 0) &
    (F.col("nominal") < 50000000)
)

# Menghitung persentil 5, 50, 95, dan 99
persentil = df_nominal_valid.approxQuantile(
    "nominal",
    [0.05, 0.50, 0.95, 0.99],
    0.01
)

# Menampilkan hasil
print("=== PERSENTIL NOMINAL TRANSAKSI VALID ===")
print(f"P5  : Rp{persentil[0]:,}")
print(f"P50 : Rp{persentil[1]:,}")
print(f"P95 : Rp{persentil[2]:,}")
print(f"P99 : Rp{persentil[3]:,}")

Interpretasi:

Berdasarkan hasil perhitungan persentil pada transaksi dengan nominal valid, diperoleh: - P5 = Rp9.500: Sebanyak 5% transaksi memiliki nominal hingga Rp9.500. - P50 = Rp39.800: Sebanyak 50% transaksi memiliki nominal hingga Rp39.800. Nilai ini merupakan median transaksi. - P95 = Rp173.600: Sebanyak 95% transaksi memiliki nominal hingga Rp173.600. - P99 = Rp2.788.700: Sebanyak 99% transaksi memiliki nominal hingga Rp2.788.700.

from pyspark.sql import functions as F
import os

# ==========================================
# C5(c) AGREGASI TRANSAKSI SUKSES PER JAM
# ==========================================

# 1. Memfilter transaksi SUKSES
df_sukses_jam = df_trx_raw.filter(
    F.col("status") == "SUKSES"
)

# 2. Menghitung jumlah transaksi berdasarkan jam
df_agregasi_jam = (
    df_sukses_jam
    .withColumn("jam", F.hour(F.col("waktu")))
    .groupBy("jam")
    .agg(
        F.count("*").alias("jumlah_transaksi")
    )
    .orderBy("jam")
)

# 3. Menampilkan hasil agregasi Spark
print("=== JUMLAH TRANSAKSI SUKSES PER JAM ===")
## === JUMLAH TRANSAKSI SUKSES PER JAM ===
df_agregasi_jam.show(24, truncate=False)
## +---+----------------+
## |jam|jumlah_transaksi|
## +---+----------------+
## |0  |15591           |
## |1  |15456           |
## |2  |15583           |
## |3  |15246           |
## |4  |15394           |
## |5  |15780           |
## |6  |15691           |
## |7  |15340           |
## |8  |15451           |
## |9  |15574           |
## |10 |15701           |
## |11 |15503           |
## |12 |15381           |
## |13 |15478           |
## |14 |15468           |
## |15 |15358           |
## |16 |15494           |
## |17 |15536           |
## |18 |15450           |
## |19 |15461           |
## |20 |15507           |
## |21 |15573           |
## |22 |15543           |
## |23 |15311           |
## +---+----------------+
# 4. Menyimpan hasil ke CSV
hasil_agregasi = df_agregasi_jam.toPandas()

hasil_agregasi.to_csv(
    "hasil_agregasi_c5.csv",
    index=False
)

print("\nFile hasil_agregasi_c5.csv berhasil disimpan.")
## 
## File hasil_agregasi_c5.csv berhasil disimpan.
print("Jumlah jam yang tercatat:", len(hasil_agregasi))
## Jumlah jam yang tercatat: 24

Interpretasi:

Berdasarkan hasil pengelompokan transaksi SUKSES berdasarkan jam, seluruh 24 jam memiliki data transaksi. Jumlah transaksi relatif stabil sepanjang hari, dengan rincian: - Transaksi tertinggi: Pukul 05.00 sebanyak 15.780 transaksi. - Transaksi terendah: Pukul 03.00 sebanyak 15.246 transaksi. - Selisih: 534 transaksi. File hasil_agregasi_c5.csv juga berhasil disimpan dan memuat hasil agregasi untuk seluruh 24 jam.

library(ggplot2)
library(dplyr)
library(scales)

# 1. Membaca hasil agregasi Spark
data_jam <- read.csv("hasil_agregasi_c5.csv")

data_jam$jam <- as.numeric(data_jam$jam)
data_jam$jumlah_transaksi <- as.numeric(data_jam$jumlah_transaksi)

# 2. Menentukan jam tertinggi dan terendah
jam_tertinggi <- data_jam %>%
  slice_max(jumlah_transaksi, n = 1)

jam_terendah <- data_jam %>%
  slice_min(jumlah_transaksi, n = 1)

# 3. Menentukan kategori warna
data_jam <- data_jam %>%
  mutate(
    kategori = case_when(
      jam == jam_tertinggi$jam ~ "Tertinggi",
      jam == jam_terendah$jam ~ "Terendah",
      TRUE ~ "Jam lainnya"
    ),
    label_jam = sprintf("%02d:00", jam)
  )

# 4. Grafik interaktif untuk HTML
if (knitr::is_html_output()) {

  library(plotly)

  grafik_jam <- plot_ly(
    data = data_jam,
    x = ~label_jam,
    y = ~jumlah_transaksi,
    type = "bar",
    color = ~kategori,
    colors = c(
      "Tertinggi" = "#16A085",
      "Terendah" = "#E67E22",
      "Jam lainnya" = "#5B8DB8"
    ),
    text = ~paste0(
      "Jam: ", label_jam,
      "<br>Jumlah transaksi: ",
      format(
        jumlah_transaksi,
        big.mark = ".",
        decimal.mark = ",",
        scientific = FALSE
      ),
      "<br>Keterangan: ", kategori
    ),
    hoverinfo = "text"
  ) %>%
    layout(
      title = list(
        text = "Distribusi Transaksi SUKSES Berdasarkan Jam",
        x = 0.5
      ),
      xaxis = list(
        title = "Jam Transaksi",
        categoryorder = "array",
        categoryarray = sprintf("%02d:00", 0:23)
      ),
      yaxis = list(
        title = "Jumlah Transaksi",
        tickformat = ",."
      ),
      legend = list(
        title = list(text = "Keterangan"),
        orientation = "h",
        x = 0.5,
        xanchor = "center",
        y = -0.2
      ),
      hovermode = "closest",
      template = "plotly_white",
      margin = list(b = 90)
    ) %>%
    config(
      displaylogo = FALSE,
      responsive = TRUE
    )

  grafik_jam

# 5. Grafik statis untuk PDF
} else {

  grafik_jam <- ggplot(
    data_jam,
    aes(x = jam, y = jumlah_transaksi, fill = kategori)
  ) +
    geom_col(width = 0.72) +

    geom_text(
      data = filter(data_jam, kategori != "Jam lainnya"),
      aes(
        label = comma(
          jumlah_transaksi,
          big.mark = ".",
          decimal.mark = ","
        )
      ),
      vjust = -0.7,
      size = 3.5,
      fontface = "bold"
    ) +

    scale_fill_manual(
      values = c(
        "Tertinggi" = "#16A085",
        "Terendah" = "#E67E22",
        "Jam lainnya" = "#5B8DB8"
      )
    ) +

    scale_x_continuous(
      breaks = 0:23,
      labels = sprintf("%02d", 0:23)
    ) +

    scale_y_continuous(
      labels = label_number(
        big.mark = ".",
        decimal.mark = ","
      ),
      expand = expansion(mult = c(0, 0.10))
    ) +

    labs(
      title = "Distribusi Transaksi SUKSES Berdasarkan Jam",
      subtitle = "Jumlah transaksi pada setiap jam (00.00–23.00)",
      x = "Jam Transaksi",
      y = "Jumlah Transaksi",
      fill = "Keterangan",
      caption = "Sumber: Hasil pengolahan data menggunakan Apache Spark"
    ) +

    theme_minimal(base_size = 11) +

    theme(
      plot.title = element_text(
        face = "bold",
        hjust = 0.5,
        size = 15
      ),
      plot.subtitle = element_text(
        hjust = 0.5,
        color = "gray40"
      ),
      legend.position = "bottom",
      panel.grid.minor = element_blank(),
      panel.grid.major.x = element_blank()
    )

  grafik_jam
}

Interpretasi:

Berdasarkan grafik, jumlah transaksi SUKSES terlihat relatif merata selama 24 jam. Batang berwarna hijau menunjukkan jumlah transaksi tertinggi pada pukul 05.00, yaitu 15.780 transaksi. Sementara itu, batang berwarna oranye menunjukkan jumlah transaksi terendah pada pukul 03.00, yaitu 15.246 transaksi.

Batang berwarna biru menunjukkan jumlah transaksi pada jam lainnya. Perbedaan tinggi batang yang tidak terlalu besar menunjukkan bahwa aktivitas transaksi berlangsung cukup stabil sepanjang hari.

C5(d) Dua Insight dari Hasil EDA

Insight 1: Aktivitas Transaksi Relatif Merata

Temuan: Jumlah transaksi SUKSES relatif stabil sepanjang hari.

Bukti: Transaksi tertinggi terjadi pukul 05.00 sebanyak 15.780 transaksi, sedangkan terendah pukul 03.00 sebanyak 15.246 transaksi. Selisihnya hanya 534 transaksi.

Tindakan: Perusahaan perlu menjaga kestabilan layanan dan kapasitas sistem agar transaksi dapat berjalan lancar sepanjang hari.

Insight 2: Sebagian Besar Transaksi Bernilai Relatif Kecil

Temuan: Sebagian besar transaksi memiliki nominal rendah, sedangkan hanya sebagian kecil yang bernilai jauh lebih tinggi.

Bukti: Median transaksi sebesar Rp39.800, sementara persentil ke-99 mencapai Rp2.788.700.

Tindakan: Perusahaan perlu memberikan perhatian lebih pada transaksi bernilai besar melalui pemantauan untuk mengidentifikasi aktivitas yang tidak biasa.