Học cách biến dữ liệu thô thành dữ liệu dùng được: tổng hợp theo nhóm, tính toán theo cửa sổ (window), ghép bảng (join), xử lý giá trị thiếu và làm sạch dữ liệu bẩn — những thao tác bạn dùng hằng ngày khi làm việc với dữ liệu thật.
sales (order_id, customer_id, category, qty, unit_price, payment, order_date) và customers (customer_id, name, region, vip). Nếu chưa, mở lại notebook Bài 1 chạy phần đọc dữ liệu trước.Tổng hợp là thao tác cơ bản nhất: gom các dòng theo một (hoặc nhiều) cột rồi tính toán trên từng nhóm — ví dụ doanh thu theo từng category. Trong PySpark, ta dùng groupBy() rồi agg().
from pyspark.sql import functions as F (sales.groupBy("category") .agg( F.sum(F.col("qty") * F.col("unit_price")).alias("revenue"), F.count("*").alias("orders"), F.avg("unit_price").alias("avg_price") ) .orderBy(F.desc("revenue")) .show())
groupBy("category") trả về một đối tượng GroupedData — chưa phải DataFrame. Bạn phải gọi tiếp agg() để biến nó về DataFrame. agg() nhận nhiều hàm tổng hợp cùng lúc, và .alias() dùng để đặt tên cột kết quả cho dễ đọc. Cuối cùng orderBy(F.desc(...)) sắp xếp giảm dần theo doanh thu.
Các hàm tổng hợp hay dùng trong module F:
| Hàm | Ý nghĩa |
|---|---|
F.sum(col) | Tổng các giá trị trong nhóm |
F.avg(col) | Giá trị trung bình (mean) |
F.count("*") | Đếm số dòng trong nhóm |
F.countDistinct(col) | Đếm số giá trị khác nhau |
F.min(col) / F.max(col) | Giá trị nhỏ nhất / lớn nhất |
F.stddev(col) | Độ lệch chuẩn |
groupBy("category", "payment"). Nếu chỉ cần một con số tổng cho cả bảng (không nhóm), dùng thẳng sales.agg(F.sum("qty")).Khác biệt mấu chốt giữa window và groupBy: groupBy gộp nhiều dòng thành một dòng kết quả, còn window giữ nguyên mọi dòng nhưng gắn thêm một giá trị tính trên "khung" (window) xung quanh dòng đó. Nhờ vậy bạn có thể xếp hạng, tính tổng lũy kế, so sánh dòng hiện tại với dòng trước/sau... mà không mất chi tiết.
from pyspark.sql.window import Window w = Window.partitionBy("category").orderBy(F.desc("amount")) (df.withColumn("rank", F.rank().over(w)) .withColumn( "running_total", F.sum("amount").over( w.rowsBetween(Window.unboundedPreceding, Window.currentRow) ) ))
partitionBy("category") chia dữ liệu thành các "khung" theo category (giống groupBy nhưng không gộp). orderBy(F.desc("amount")) sắp thứ tự trong mỗi khung. F.rank().over(w) gán hạng cho từng dòng. Phần rowsBetween(unboundedPreceding, currentRow) định nghĩa khung "từ dòng đầu đến dòng hiện tại" — chính là công thức tính running total (tổng lũy kế).
Các hàm window thường gặp: rank, dense_rank, row_number (xếp hạng), lag/lead (lấy giá trị dòng trước/sau), và mọi hàm tổng hợp (sum, avg...) khi gọi qua .over(w).
Ba hàm xếp hạng khác nhau ở cách xử lý giá trị bằng nhau (tie):
| Hàm | Khi có giá trị bằng nhau | Ví dụ (100, 100, 90) |
|---|---|---|
rank() | Cùng hạng, nhảy số hạng tiếp theo | 1, 1, 3 |
dense_rank() | Cùng hạng, không nhảy số | 1, 1, 2 |
row_number() | Luôn duy nhất, không có hạng bằng nhau | 1, 2, 3 |
row_number() (đảm bảo mỗi nhóm lấy đúng N dòng); cần xếp hạng kiểu thi đấu thì dùng rank(); cần hạng liên tục không lỗ hổng thì dense_rank().Join dùng để gắn thông tin từ bảng này sang bảng khác qua một cột khóa chung. Ở đây ta ghép sales với customers qua customer_id để biết mỗi đơn hàng thuộc khách nào, vùng nào.
joined = sales.join(customers, on="customer_id", how="inner") joined.show()
Tham số on là cột (hoặc danh sách cột) khóa chung; how quyết định kiểu join:
| how | Giữ lại những dòng nào |
|---|---|
inner | Chỉ dòng có khóa khớp ở cả hai bảng (mặc định) |
left | Tất cả dòng bảng trái + phần khớp bên phải (không khớp → null) |
right | Tất cả dòng bảng phải + phần khớp bên trái |
outer | Tất cả dòng của cả hai bảng, chỗ thiếu điền null |
left_anti | Chỉ dòng bảng trái không tìm thấy khớp bên phải (lọc ngược) |
left_semi | Dòng bảng trái có khớp, nhưng chỉ giữ cột bảng trái |
Reference is ambiguous. Cách xử lý: dùng dạng on="customer_id" (thay vì điều kiện sales.customer_id == customers.customer_id) để Spark gộp cột khóa thành một; hoặc rename cột trùng trước khi join. Với bảng nhỏ (như customers), bọc F.broadcast(customers) để Spark gửi nguyên bảng nhỏ tới mọi node — nhanh hơn nhiều, ta sẽ học kỹ ở Bài 6.Dữ liệu thật luôn có ô trống (null). Spark cho bạn 3 hướng xử lý chính: bỏ dòng, điền giá trị, hoặc lọc/thay thế theo điều kiện.
# bỏ những dòng có bất kỳ null nào df.na.drop() # chỉ bỏ dòng null ở một số cột df.na.drop(subset=["customer_id", "qty"]) # điền giá trị mặc định cho cột qty df.na.fill({"qty": 0, "region": "unknown"}) # lọc ra các dòng có cột x bị null df.filter(F.col("x").isNull()) # coalesce: lấy giá trị không-null đầu tiên df.withColumn("price", F.coalesce(F.col("unit_price"), F.lit(0.0)))
| Cách | Dùng khi |
|---|---|
df.na.drop() / dropna() | Muốn bỏ hẳn dòng thiếu dữ liệu. Thêm subset=[...] để chỉ xét vài cột. |
df.na.fill({...}) / fillna() | Điền giá trị mặc định, có thể chỉ định riêng từng cột bằng dict. |
col.isNull() / isNotNull() | Lọc / kiểm tra null trong điều kiện. |
F.coalesce(a, b, ...) | Trả về giá trị không-null đầu tiên trong danh sách cột. |
Nhớ rằng sales.csv cố tình có vài dòng bẩn — ví dụ qty âm (không hợp lệ), đơn trùng, kiểu dữ liệu sai. Đây là code làm sạch điển hình, nối nhiều bước transform liền mạch:
clean = (sales
.filter(F.col("qty") > 0)
.dropDuplicates(["order_id"])
.withColumn("unit_price", F.col("unit_price").cast("double"))
.withColumn("order_date", F.to_date("order_date", "yyyy-MM-dd")))
clean.printSchema()
clean.show(5)
Diễn giải từng bước: filter(F.col("qty") > 0) loại bỏ dòng lỗi qty âm; dropDuplicates(["order_id"]) bỏ đơn trùng (giữ một dòng cho mỗi order_id); cast("double") ép cột giá về kiểu số thực (CSV đọc vào hay là chuỗi); to_date("order_date", "yyyy-MM-dd") parse chuỗi ngày thành kiểu date thực sự để sau này tính toán theo thời gian.
Quy trình làm sạch dữ liệu chuẩn thường gồm các bước:
df.summary(), df.printSchema() để hiểu phân bố và phát hiện giá trị lạ.dropDuplicates theo khóa định danh (order_id).cast số, to_date/to_timestamp cho ngày giờ, trim/lower cho chuỗi.drop hoặc fill tùy cột, rồi kiểm tra lại số dòng còn lại.show, count, write). Nếu kết quả không như mong đợi, kiểm tra lại thứ tự các bước trước khi nghi ngờ dữ liệu.Dùng sales và customers đã load, tự viết code cho 3 bài sau:
sales với customers theo customer_id, rồi groupBy("region") và tính tổng qty * unit_price. Sắp xếp giảm dần để xem vùng nào bán mạnh nhất.amount = qty * unit_price, dùng window partitionBy("category").orderBy(F.desc("amount")) với row_number(), rồi filter giữ những dòng có hạng ≤ 3.customers, lọc vip == True rồi groupBy("region").count(). So sánh với tổng số khách mỗi vùng để ra tỷ lệ VIP.row_number() bằng rank() và quan sát khác biệt khi có hai đơn cùng amount — đó là cách tốt nhất để nhớ sự khác nhau giữa chúng.