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.
B1. Jelaskan perbedaan scale-up dan scale-out, dan jelaskan mengapa fase shuffle pada MapReduce/Spark merupakan bagian yang paling mahal.
Jawaban:
Skalabilitas Vertikal (Scale-up) vs Horizontal (Scale-out):
Mengapa Fase Shuffle Paling Mahal:
Fase shuffle merupakan proses redistribusi dan pertukaran data antarpartisi yang berada di mesin (nodes) yang berbeda di dalam klaster. Proses ini sangat mahal karena memerlukan operasi pembacaan dan penulisan berkas sementara ke media penyimpanan (disk I/O) serta transfer data melalui jaringan (network I/O) yang dapat menimbulkan hambatan (bottleneck).
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:
Katalog Produk dengan Atribut Berbeda: Basis Data Dokumen (Document Store, contoh: MongoDB)
Keranjang Belanja Sementara: Basis Data Kunci-Nilai (Key-Value Store, contoh: Redis)
Rekomendasi “Teman Anda Juga Membeli”: Basis Data Graf (Graph Database, contoh: Neo4j)
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:
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.
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:
C1.1 Merchant Kuliner Cashback di Bandung Ditemukan satu merchant yang memenuhi kriteria, yaitu Merchant 75 dengan rating 4,1 dan memiliki promo cashback.
C1.2 Merchant dengan Atribut jam_buka Terdapat 61 merchant yang memiliki atribut jam_buka dalam data.
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.
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.
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%.
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.
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.
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.
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.