Giao diện
Để phân tích dữ liệu hiệu quả, người làm dữ liệu cần kết nối các nguồn bảng rời rạc bằng các phép toán quan hệ. Đồng thời, cấu trúc bảng cũng cần chuyển đổi linh hoạt giữa dạng dài (Long format) và dạng rộng (Wide format) cho các bước vẽ đồ thị hay huấn luyện mô hình. Bài học này trình bày có hệ thống các kỹ thuật: Tổ chức dữ liệu đa chiều bằng phân cấp, ghép bảng quan hệ, nối trục và xoay hình dạng bảng trong pandas.
1. Chỉ mục phân cấp (Hierarchical Indexing / MultiIndex)
phân cấp (Hierarchical Indexing, được hiện thực hóa qua lớp MultiIndex trong pandas) là cơ chế cho phép bạn sở hữu hai hoặc nhiều tầng chỉ mục trên cùng một trục. Nhờ đó, ta có thể thao tác với dữ liệu nhiều chiều (3 chiều, 4 chiều) trong khuôn khổ của cấu trúc hai chiều quen thuộc.
1.1. Khởi tạo và trích chọn dữ liệu với .loc
Ta có thể khởi tạo một MultiIndex từ danh sách các bộ giá trị (tuples), mảng song song, hoặc tích Descartes giữa các tập nhãn (product):
python
import numpy as np
import pandas as pd
# Khởi tạo MultiIndex từ danh sách các cặp (khu_vuc, nam)
cac_cap = [
("Bắc", 2025), ("Bắc", 2026),
("Nam", 2025), ("Nam", 2026)
]
chi_muc = pd.MultiIndex.from_tuples(cac_cap, names=["khu_vuc", "nam"])
du_lieu = pd.Series([120, 150, 200, 260], index=chi_muc)
print("Series với chỉ mục phân cấp:")
print(du_lieu)Điểm mạnh vượt trội của MultiIndex là khả năng trích chọn bộ phận (Partial Indexing):
- Truy xuất tầng ngoài cùng: Cú pháp
du_lieu.loc["Bắc"]sẽ trích xuất toàn bộ dữ liệu của khu vực Bắc dưới dạng một Series thông thường với lànam. - Truy xuất phần tử cụ thể: Truyền một bộ giá trị
du_lieu.loc[("Bắc", 2026)]. - Truy xuất tầng bên trong: Sử dụng toán tử cắt lát kết hợp với
pd.IndexSlice:
python
print("Dữ liệu khu vực Bắc:")
print(du_lieu.loc["Bắc"])
assert du_lieu.loc[("Bắc", 2026)] == 1501.2. Đảo và sắp xếp các tầng chỉ mục
Khi làm việc với phân cấp, thứ tự sắp xếp của các tầng đóng vai trò quyết định đến hiệu năng truy xuất:
swaplevel(): Hoán đổi vị trí giữa hai tầng cụ thể.sort_index(level=...): Sắp xếp các giá trị theo thứ tự từ điển tại một tầng được chỉ định.
Nguyên tắc sắp xếp chỉ mục phân cấp
Các phép trích chọn bộ phận và cắt lát dải nhãn trên MultiIndex sẽ đạt hiệu năng cao nhất và không bao giờ gặp cảnh báo lỗi khi đã được sắp xếp từ điển tăng dần bằng phương thức sort_index().
python
du_lieu_dao = du_lieu.swaplevel("khu_vuc", "nam").sort_index()
print("Series sau khi đảo tầng và sắp xếp:")
print(du_lieu_dao)
assert du_lieu_dao.index.names == ["nam", "khu_vuc"]1.3. Thống kê tổng hợp theo tầng chỉ mục
Thay vì phải phá vỡ cấu trúc bảng, các phương thức thống kê tổng hợp của pandas cho phép thực hiện phép tính trực tiếp trên một tầng thông qua phương thức groupby(level=...):
python
# Tính tổng doanh thu theo từng khu vực bất kể năm:
tong_theo_khu = du_lieu.groupby(level="khu_vuc").sum()
print("Tổng doanh thu theo khu vực:")
print(tong_theo_khu)
assert tong_theo_khu["Bắc"] == 270
assert tong_theo_khu["Nam"] == 4601.4. Chuyển đổi qua lại giữa cột và chỉ mục
Hai phương thức cơ bản giúp luân chuyển dữ liệu giữa các cột thông thường và :
set_index([cot_1, cot_2]): Đưa một hoặc nhiều cột hiện tại lên làm phân cấp (sử dụngdrop=Falsenếu muốn giữ lại các cột đó trong phần thân dữ liệu).reset_index(): Đưa các tầng trở lại thành các cột dữ liệu thông thường, chuyển chỉ mục hàng về dạng số tự tăngRangeIndex.
2. Ghép và nối các tập dữ liệu (Combining and Merging)
Việc kết hợp nhiều bảng dữ liệu được thực hiện thông qua ba cơ chế toán học rạch ròi: Phép ghép theo kiểu cơ sở dữ liệu quan hệ (merge), phép nối dọc theo một trục tọa độ (concat), và phép ghép bù đắp dữ liệu khuyết (combine_first).
2.1. Phép ghép bảng kiểu cơ sở dữ liệu với pd.merge
Hàm pd.merge() kết nối các hàng của hai dựa trên một hoặc nhiều khóa chung (keys), tương đương với mệnh đề JOIN trong ngôn ngữ SQL:
on: Tên cột làm khóa ghép khi cả hai bảng đều dùng chung một tên cột.left_on,right_on: Chỉ định tên cột khóa riêng biệt khi hai bảng đặt tên cột khác nhau.how: Phương thức ghép nối:"inner"(mặc định): Phép giao (Inner Join), chỉ giữ lại các bản ghi có khóa xuất hiện đồng thời ở cả hai bảng."left": Phép ghép trái (Left Join), giữ lại toàn bộ các hàng của bảng bên trái, còn các hàng không tìm thấy đối tác bên phải sẽ nhận giá trịNaN."right": Phép ghép phải (Right Join), giữ lại toàn bộ các hàng của bảng bên phải."outer": Phép hợp (Outer Join), giữ lại toàn bộ các bản ghi của cả hai bảng, trong đó phần thiếu ở mỗi bên được điềnNaN.
suffixes=("_trai", "_phai"): Hậu tố tự động gán vào tên các cột không phải khóa nhưng bị trùng tên giữa hai bảng.
Bản số ghép nối và nhân số hàng (Merge Cardinality)
Quan hệ giữa các bảng khi ghép có thể thuộc một trong bốn dạng bản số:
- Một - Một (1:1): Mỗi giá trị khóa xuất hiện duy nhất một lần ở mỗi bảng.
- Một - Nhiều (1:N) hoặc Nhiều - Một (N:1): Khóa ở một bảng là duy nhất (chẳng hạn bảng danh mục sản phẩm), còn ở bảng kia xuất hiện lặp lại (bảng nhật ký giao dịch).
- Nhiều - Nhiều (N:M): Khóa xuất hiện lặp lại ở cả hai bảng. Khi một giá trị khóa xuất hiện
lần ở bảng trái và lần ở bảng phải, phép ghép sẽ tạo ra tích Descartes nội bộ gồm hàng cho riêng giá trị khóa đó.
Hành vi của khóa mang giá trị khuyết thiếu (NaN)
Trong ngôn ngữ SQL chuẩn, NULL biểu thị dữ liệu chưa biết nên biểu thức so sánh NULL = NULL trả về UNKNOWN, làm cho các hàng mang khóa NULL không bao giờ ghép khớp với nhau. Ngược lại, trong pandas pd.merge(), giá trị khuyết thiếu (NaN) mặc định được đối xử như hai giá trị bằng nhau. Nếu cả hai bảng đều chứa các hàng mang khóa NaN, phép ghép sẽ nối các dòng NaN lại với nhau và sinh ra tích Descartes giữa các bản ghi thiếu khóa:
python
bang_a = pd.DataFrame({"khoa": ["K1", "K1", np.nan], "gt_a": [1, 2, 3]})
bang_b = pd.DataFrame({"khoa": ["K1", "K1", np.nan], "gt_b": [10, 20, 30]})
# Khóa 'K1' sinh 2x2 = 4 hàng, khóa NaN sinh 1x1 = 1 hàng: Tổng 5 hàng
bang_ghep_khoa = pd.merge(bang_a, bang_b, on="khoa", how="inner")
print("Bảng ghép với khóa có NaN và trùng lặp:")
print(bang_ghep_khoa)
assert len(bang_ghep_khoa) == 5
assert bang_ghep_khoa["khoa"].isna().sum() == 1Nếu bài toán nghiệp vụ không coi các bản ghi thiếu khóa là cùng một thực thể, bạn cần lọc bỏ hoặc xử lý NaN trên cột khóa trước khi thực hiện ghép bảng.
python
bang_sinh_vien = pd.DataFrame({
"ma_sv": ["SV01", "SV02", "SV03"],
"ho_ten": ["An", "Bình", "Cường"],
"khoa": ["CNTT", "Kinh tế", "CNTT"]
})
bang_diem = pd.DataFrame({
"ma_sv": ["SV01", "SV02", "SV04"],
"diem_gpa": [3.6, 3.2, 3.8]
})
# Ghép toàn bộ sinh viên và điểm số bằng phép Left Join
df_hop_nhat = pd.merge(bang_sinh_vien, bang_diem, on="ma_sv", how="left")
print("Bảng sau phép Left Join:")
print(df_hop_nhat)
assert len(df_hop_nhat) == 3
assert pd.isna(df_hop_nhat.loc[df_hop_nhat["ma_sv"] == "SV03", "diem_gpa"].iloc[0])2.2. Kiểm thực cấu trúc ghép nối với tham số validate
Một trong những sai lầm nghiêm trọng nhất khi ghép bảng là hiện tượng bùng nổ số hàng do tích Descartes ngầm (Cartesian Explosion). Giả sử bạn muốn gắn thông tin danh mục vào bảng đơn hàng (kỳ vọng quan hệ nhiều - một). Nếu vô tình bảng danh mục bị trùng lặp khóa, mỗi đơn hàng sẽ bị nhân đôi hoặc nhân ba số dòng, làm sai lệch toàn bộ báo cáo doanh thu kế toán!
Để phòng vệ tuyệt đối, pandas cung cấp tham số validate:
validate="one_to_one": Kiểm tra khóa ở cả hai bảng đều là duy nhất tuyệt đối.validate="one_to_many": Khóa ở bảng trái là duy nhất, bảng phải có thể lặp.validate="many_to_one": Khóa ở bảng trái có thể lặp, bảng phải bắt buộc là duy nhất.validate="many_to_many": Cho phép quan hệ nhiều - nhiều ở cả hai bên.
Nếu cấu trúc khóa thực tế vi phạm khai báo, pandas sẽ lập tức ném ra lỗi MergeError để dừng tiến trình, ngăn chặn việc sản sinh dữ liệu sai:
python
try:
# Bảng tra cứu không được phép trùng lặp khóa:
tra_cuu_loi = pd.DataFrame({"ma_sv": ["SV01", "SV01"], "hoc_bong": [500, 700]})
pd.merge(bang_sinh_vien, tra_cuu_loi, on="ma_sv", validate="many_to_one")
except pd.errors.MergeError:
print("Bắt lỗi thành công: Khóa bảng tra cứu bị trùng lặp!")2.3. Ghép nối dựa trên chỉ mục hàng (join)
Khi một hoặc cả hai bảng muốn sử dụng chính hàng (Index) làm khóa ghép, ta có thể chỉ định left_index=True hoặc right_index=True. Ngoài ra, phương thức tiện ích df.join() mặc định sử dụng chỉ mục hàng để ghép nối, rất thuận tiện khi hợp nhất nhiều có chung chỉ mục.
2.4. Nối dữ liệu dọc theo trục với pd.concat
Khác với merge dựa trên giá trị khóa, hàm pd.concat() thực hiện thao tác dán ghép vật lý các mảng dữ liệu dọc theo một trục tọa độ:
axis=0(mặc định): Nối theo chiều dọc (thêm các hàng mới vào bên dưới).axis=1: Nối theo chiều ngang (thêm các cột mới vào bên phải).join="inner": Chỉ giữ lại các cột (hoặc hàng) xuất hiện ở cả hai bên, trong khijoin="outer"(mặc định) giữ lại tất cả và điềnNaNvào phần chênh lệch.ignore_index=True: Xóa bỏ cũ và đánh số lại chỉ mục từđến , loại trừ nguy cơ trùng lặp chỉ mục hàng. keys=[...]: Tạo một tầng phân cấp mới ở ngoài cùng để đánh dấu xuất xứ của từng khối dữ liệu được ghép:
python
thang_1 = pd.DataFrame({"doanh_thu": [100, 150]}, index=["SP1", "SP2"])
thang_2 = pd.DataFrame({"doanh_thu": [120, 180]}, index=["SP1", "SP2"])
# Nối theo chiều dọc và phân tầng theo tháng
df_noi = pd.concat([thang_1, thang_2], keys=["T1", "T2"])
print("Bảng nối với MultiIndex:")
print(df_noi)
assert df_noi.loc[("T2", "SP1"), "doanh_thu"] == 1202.5. Ghép chồng dữ liệu có phần khuyết bằng combine_first
Trong các bài toán làm sạch dữ liệu, ta thường có một nguồn dữ liệu chính nhưng bị khuyết thiếu một số ô, và một nguồn dữ liệu phụ dùng để chắp vá. Phương thức df1.combine_first(df2) sẽ thực hiện cơ chế: Giữ nguyên các giá trị hợp lệ của df1, và chỉ lấy giá trị từ df2 để lấp đầy những vị trí mà df1 đang mang giá trị NaN:
python
du_lieu_chinh = pd.Series([np.nan, 2.5, np.nan, 3.5, 4.5], index=list("abcde"))
du_lieu_phu = pd.Series([1.0, 9.9, 3.0, 9.9, 9.9], index=list("abcde"))
ket_qua_va = du_lieu_chinh.combine_first(du_lieu_phu)
print("Dữ liệu sau khi vá ô trống:")
print(ket_qua_va)
assert ket_qua_va["a"] == 1.0
assert ket_qua_va["b"] == 2.53. Chuyển dạng và xoay bảng (Reshaping and Pivoting)
Cấu trúc hình học của dữ liệu quyết định khả năng tính toán và trực quan hóa:
- Dạng dài (Long format / Tidy data): Mỗi quan sát là một hàng riêng biệt, các biến số được biểu diễn bằng cột định danh và cột giá trị. Định dạng này lý tưởng cho các thư viện phân tích nhóm và đồ họa hiện đại như Seaborn.
- Dạng rộng (Wide format): Các mức giá trị của một biến được trải rộng thành các cột riêng biệt, thuận tiện cho mắt người đọc báo cáo tổng hợp.
3.1. Chuyển dạng với stack và unstack
Cặp phương thức stack() và unstack() luân chuyển dữ liệu qua lại giữa cột và hàng dựa trên phân cấp:
unstack(): Nhấc một tầng của hàng (Row Index) xoay lên làm tiêu đề cột (Column Header), biến bảng từ dạng dài thành dạng rộng hơn.stack(): Gom các tiêu đề cột nén ngược trở lại thành một tầng của hàng, biến bảng từ dạng rộng thành dạng dài hơn.
python
bang_goc = pd.DataFrame(
[[10, 20], [30, 40]],
index=["Hà Nội", "Đà Nẵng"],
columns=["Quý 1", "Quý 2"]
)
# Chuyển từ dạng rộng sang dạng dài bằng stack:
bang_dai = bang_goc.stack()
print("Bảng sau khi stack:")
print(bang_dai)
# Khôi phục lại dạng ban đầu bằng unstack:
bang_khoi_phuc = bang_dai.unstack()
assert bang_khoi_phuc.equals(bang_goc)3.2. Chuyển dạng dài sang dạng rộng với pivot
Phương thức pivot(index=..., columns=..., values=...) cho phép tái cấu trúc một bảng dạng dài thành bảng dạng rộng dựa trên các cột nghiệp vụ:
index: Cột sẽ trở thành hàng của bảng mới.columns: Cột có các giá trị phân loại sẽ được bung ra thành các tiêu đề cột mới.values: Cột chứa dữ liệu số học sẽ được điền vào phần thân bảng.
python
nhat_ky_gia = pd.DataFrame({
"ngay": ["2026-01-01", "2026-01-01", "2026-01-02", "2026-01-02"],
"ma_cp": ["FPT", "VNM", "FPT", "VNM"],
"gia_dong_cua": [120, 75, 122, 74]
})
bang_gia_rong = nhat_ky_gia.pivot(index="ngay", columns="ma_cp", values="gia_dong_cua")
print("Bảng giá dạng rộng:")
print(bang_gia_rong)
assert bang_gia_rong.loc["2026-01-02", "FPT"] == 122Điều kiện tiên quyết của `pivot`
Phương thức pivot bắt buộc mỗi cặp (index, columns) chỉ được xuất hiện duy nhất một lần. Nếu tồn tại nhiều bản ghi trùng lặp cho cùng một cặp và cột, pivot sẽ báo lỗi ValueError. Trong tình huống có dữ liệu trùng lặp cần tổng hợp, ta bắt buộc phải sử dụng pivot_table (sẽ học ở bài học tổng hợp nhóm).
3.3. Chuyển dạng rộng sang dạng dài với melt
Ngược lại với pivot, phương thức pd.melt() cho phép nén nhiều cột thành một cặp cột duy nhất gồm "tên thuộc tính" và "giá trị":
id_vars: Danh sách các cột đóng vai trò là mã định danh, được giữ nguyên vị trí.value_vars: Danh sách các cột cần nén lại thành hàng (nếu bỏ qua, pandas sẽ nén toàn bộ các cột còn lại).var_name: Tên của cột mới lưu trữ tên các thuộc tính cũ.value_name: Tên của cột mới lưu trữ giá trị số học.
python
bang_doanh_so = pd.DataFrame({
"chi_nhanh": ["CN1", "CN2"],
"Thang1": [500, 700],
"Thang2": [600, 800]
})
bang_chay_dai = pd.melt(
bang_doanh_so,
id_vars=["chi_nhanh"],
value_vars=["Thang1", "Thang2"],
var_name="thoi_gian",
value_name="doanh_so"
)
print("Bảng sau khi melt:")
print(bang_chay_dai)
assert len(bang_chay_dai) == 4
assert bang_chay_dai["doanh_so"].sum() == 26005. Tổng kết
- Làm chủ phân cấp: Sử dụng
MultiIndexđể quản lý dữ liệu nhiều chiều trên cấu trúc hai chiều, đồng thời nên gọisort_index()để tối ưu hóa hiệu năng trích chọn bộ phận với.loc. - Kỷ luật khi ghép bảng với
merge: Luôn định hình rõ mối quan hệ giữa các bảng (, , ) và sử dụng tham số validateđể ngăn ngừa nguy cơ bùng nổ số hàng do tích Descartes ngoài ý muốn. - Phân biệt rạch ròi giữa
mergevàconcat: Sử dụngmergekhi cần kết nối dữ liệu dựa trên giá trị của các cột khóa chung, còn sử dụngconcatkhi cần dán ghép các mảng dữ liệu vật lý dọc theo chiều hàng hoặc chiều cột. - Vá dữ liệu với
combine_first: Tận dụng cơ chế ghép chồng để lấp đầy các ô trống trong bảng dữ liệu chính bằng thông tin từ nguồn phụ trợ mà không ghi đè lên các giá trị hợp lệ đã có. - Linh hoạt chuyển dạng dữ liệu: Sử dụng
stack/unstackcho cấu trúc có phân cấp, đồng thời chuyển đổi thuần thục giữa dạng dài và dạng rộng bằngpivotvàmeltđể đáp ứng yêu cầu của từng giai đoạn phân tích.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 8: Data Wrangling: Join, Combine, and Reshape.
Tài liệu chính thức pandas: Merge, join, concatenate and compare & Reshaping and pivot tables.
Hadley Wickham, Tidy Data, Journal of Statistical Software, 2014.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
4. Bài tập rèn luyện
Bài tập 1: Ghép trái khi khóa lặp ở cả hai bảng
Cho hai bảng dữ liệu độc lập về hệ thống phòng nghỉ và khu vực địa lý:
Bảng trai:
| id | khu |
|---|---|
| P1 | A |
| P2 | A |
| P3 | B |
Bảng phai:
| khu | vung |
|---|---|
| A | Bắc |
| A | Trung tâm |
Hãy viết câu lệnh ghép hai bảng trên bằng phương thức Left Join trên cột khóa khu. Xác định rõ cấu trúc, số lượng hàng và giá trị cụ thể của từng hàng trong bảng kết quả. Giải thích cơ chế khớp dòng của thuật toán.
Lời giải
Câu lệnh thực thi trong pandas:
python
trai = pd.DataFrame({
"id": ["P1", "P2", "P3"],
"khu": ["A", "A", "B"]
})
phai = pd.DataFrame({
"khu": ["A", "A"],
"vung": ["Bắc", "Trung tâm"]
})
ket_qua_ghep = trai.merge(phai, on="khu", how="left")
print("Bảng kết quả sau phép Left Join:")
print(ket_qua_ghep)
assert len(ket_qua_ghep) == 5
assert ket_qua_ghep["vung"].tolist() == ["Bắc", "Trung tâm", "Bắc", "Trung tâm", np.nan] or \
set(zip(ket_qua_ghep["id"], ket_qua_ghep["vung"])) == {
("P1", "Bắc"), ("P1", "Trung tâm"),
("P2", "Bắc"), ("P2", "Trung tâm"),
("P3", np.nan)
}Bảng kết quả gồm chính xác
(P1, A, Bắc)(P1, A, Trung tâm)(P2, A, Bắc)(P2, A, Trung tâm)(P3, B, NaN)
Phân tích cơ chế kỹ thuật:
- Hàng đầu tiên của bảng trái (
P1,A) khớp với hai hàng có mãAbên bảng phải, do đó sản sinh ra hai hàng tương ứng với vùngBắcvàTrung tâm. - Hàng thứ hai của bảng trái (
P2,A) cũng khớp với hai hàng bên bảng phải, tiếp tục sản sinh thêm hai hàng. - Hàng thứ ba của bảng trái (
P3,B) không tìm thấy mãBở bảng phải. Tuy nhiên, vì sử dụng phương thức ghéphow="left", toàn bộ các hàng của bảng trái bắt buộc phải được bảo toàn. Do đó, dòngP3được giữ lại và trường thuộc tínhvungđược điền giá trị khuyết thiếuNaN.
Ghép bảng tra cứu và kiểm định quan hệ dữ liệu trong pandas
Bảng giá và bảng phí thường được lưu riêng. Ta cần ghép đúng phân khúc, kiểm tra khóa có bị lặp hay thiếu, rồi tính giá sau phí mà vẫn giữ đủ các dòng cần phân tích.
1. Thiết lập môi trường và dữ liệu thực hành
Để bảo đảm quy trình ghép bảng có thể kiểm chứng độc lập, môi trường thực hành được khởi tạo với bảng dữ liệu danh mục phòng 12 dòng (DEMO, đơn vị giá: CLP/đêm) và bảng tra cứu biểu phí dịch vụ (phi) gồm ba dòng tương ứng với ba phân khúc giá.
python
import numpy as np
import pandas as pd
# 12 dòng dữ liệu mẫu giả lập danh mục phòng (đơn vị: CLP/đêm)
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
# Hàm phân loại dải giá hỗ trợ gán phân khúc
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
# Bổ sung cột phân khúc giá cho bảng dữ liệu mẫu
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bảng tra cứu biểu phí dịch vụ theo phân khúc
phi = pd.DataFrame(
{
"phan_khuc": ["re", "trung", "cao"],
"phi_dich_vu": [0.10, 0.13, 0.15],
}
)
# Địa chỉ lưu trữ snapshot thực tế Inside Airbnb
SNAPSHOT_URL = (
"https://data.insideairbnb.com/chile/rm/santiago/"
"2026-06-29/visualisations/listings.csv"
)NOTE
Biểu phí 10%, 13% và 15% là giả định của bài tập để luyện ghép bảng. Nó không phải biểu phí thực tế của nền tảng hoặc chủ nhà.
Đối chiếu thực tế: Trên snapshot thực tế toàn thành phố Santiago gồm 18.534 niêm yết, bảng tra cứu biểu phí gồm 3 dòng được ghép vào bảng danh mục phòng. Sau khi thực hiện phép ghép trái, bảng kết quả giữ nguyên 18.534 dòng và ghi nhận chính xác 846 giá trị
NaNở cộtphi_dich_vu(tương ứng đúng với 846 phòng bị khuyết giá nên không xác định được phân khúc). Mức giá trung vị sau khi tính cả phí dịch vụ của phân khúc trung bình đạt 61.897 CLP.
2. Nội dung chi tiết bài tập thực hành
Bài tập 10: Ghép bảng tra cứu biểu phí dịch vụ và kiểm định quan hệ với validate='m:1'
Bài tập 10: Ghép bảng tra cứu và kiểm định quan hệ nhiều - một
Giả sử nền tảng đặt phòng áp dụng chính sách thu phí dịch vụ dựa trên phân khúc giá:
- Phân khúc rẻ: Mức phí 10% (0,10).
- Phân khúc trung bình: Mức phí 13% (0,13).
- Phân khúc cao: Mức phí 15% (0,15).
Cho bảng danh mục phòng df chứa cột phan_khuc và bảng tra cứu phi gồm hai cột phan_khuc và phi_dich_vu.
Hãy viết hàm merge_fees(df: pd.DataFrame, phi: pd.DataFrame) -> dict ghép hai bảng trên theo cột khóa phan_khuc bằng phương thức ghép trái (how="left"), kèm theo tham số kiểm định quan hệ validate="m:1". Hàm trả về một từ điển gồm đúng ba khóa sau:
m: Đối tượng sau khi ghép nối, giữ nguyên tất cả các cột củadfvà bổ sung thêm cộtphi_dich_vu.so_dong_m: Số nguyên (int) thể hiện tổng số dòng của bảng kết quảm.so_nan_phi: Số nguyên (int) đếm số lượng giá trịNaNxuất hiện trong cộtphi_dich_vusau khi ghép.
Yêu cầu kỹ thuật:
- Thiết lập
validate="m:1"để kiểm tra tính duy nhất của khóa trong bảng tra cứu bên phải. Nếu bảng tra cứuphichứa khóa trùng lặp, phải để ngoại lệpd.errors.MergeErrorđược ném ra một cách tự nhiên (không tự ý bắt lỗi hay khử trùng lặp bên trong hàm). - Không làm thay đổi cấu trúc và dữ liệu của hai
dfvàphitruyền vào.
Lời giải
Cách 1: Ghép trái và kiểm tra quan hệ nhiều dòng với một khóa tra cứu
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
phi = pd.DataFrame(
{
"phan_khuc": ["re", "trung", "cao"],
"phi_dich_vu": [0.10, 0.13, 0.15],
}
)
# Bắt đầu lời giải
import pandas as pd
def merge_fees(df: pd.DataFrame, phi: pd.DataFrame) -> dict:
# 1. Thực hiện ghép trái với kiểm định quan hệ nhiều - một (many-to-one)
m = pd.merge(df, phi, on="phan_khuc", how="left", validate="m:1")
# 2. Đếm số dòng sau khi ghép
so_dong_m = len(m)
# 3. Đếm số lượng ô khuyết trong cột phí dịch vụ
so_nan_phi = int(m["phi_dich_vu"].isna().sum())
return {
"m": m,
"so_dong_m": so_dong_m,
"so_nan_phi": so_nan_phi,
}
# Gọi thử nghiệm hàm trên bảng DEMO và bảng tra cứu phi:
kq_merge_c1 = merge_fees(DEMO, phi)
print("Kết quả Cách 1:")
print("Số dòng sau ghép:", kq_merge_c1["so_dong_m"])
print("Số ô NaN ở cột phí:", kq_merge_c1["so_nan_phi"])
print("Các cột của bảng sau ghép:", list(kq_merge_c1["m"].columns))
# Kết thúc lời giảiGiải thích Cách 1
on="phan_khuc" chỉ khóa ghép chung. how="left" giữ các dòng của df ngay cả khi không tìm được mức phí. Với bảng phải có khóa duy nhất, mỗi dòng trái chỉ khớp nhiều nhất một dòng phải, nên DEMO vẫn có 12 dòng sau ghép.
validate="m:1" kiểm tra tính duy nhất của khóa bên phải. Ký hiệu m cho phép nhiều dòng trái cùng phân khúc, còn 1 yêu cầu một dòng tra cứu cho mỗi khóa. Nếu bỏ kiểm tra và để hai dòng phí cùng khóa, một dòng trái có thể sinh thành hai dòng kết quả. Bảng sẽ tăng số dòng và làm sai các phép tổng hợp tiếp theo.
len(m) đếm số dòng sau ghép. .isna().sum() trên cột phí đếm các khóa chưa khớp. Id 204 không có giá, dẫn đến phân khúc None, trong khi bảng phi không có khóa khuyết nên mức phí của dòng ấy là NaN. pandas có thể ghép hai khóa khuyết với nhau nếu cả hai phía đều có khóa khuyết, vì vậy kết luận này phụ thuộc đúng bảng tra cứu đang dùng.
Output Cách 1
| Đại lượng | Kết quả |
|---|---|
so_dong_m | 12 |
so_nan_phi | 1 |
| Cột được bổ sung | phi_dich_vu |
| Id chưa khớp phí | 204 |
Cách 2: Dùng phương thức merge và kiểm tra giá sau phí
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
phi = pd.DataFrame(
{
"phan_khuc": ["re", "trung", "cao"],
"phi_dich_vu": [0.10, 0.13, 0.15],
}
)
# Bắt đầu lời giải
import pandas as pd
def merge_fees(df: pd.DataFrame, phi: pd.DataFrame) -> dict:
# Sử dụng phương thức .merge() của đối tượng DataFrame
m = df.merge(phi, on="phan_khuc", how="left", validate="many_to_one")
so_dong_m = len(m)
so_nan_phi = int(m["phi_dich_vu"].isna().sum())
return {
"m": m,
"so_dong_m": so_dong_m,
"so_nan_phi": so_nan_phi,
}
# Gọi thử nghiệm hàm trên bảng DEMO và bảng tra cứu phi:
kq_merge_c2 = merge_fees(DEMO, phi)
m_ket_qua = kq_merge_c2["m"]
# Tính toán giá tổng cộng gồm phí và trung vị giá theo phân khúc:
gia_tong = m_ket_qua["price"] * (1 + m_ket_qua["phi_dich_vu"])
trung_vi_gia_tong = gia_tong.groupby(m_ket_qua["phan_khuc"]).median().round(0)
print("Trung vị giá gồm phí theo phân khúc Cách 2:")
print(trung_vi_gia_tong)
# Thử nghiệm kích hoạt kiểm định validate='m:1' khi bảng tra cứu bị lỗi trùng khóa:
phi_trung = pd.concat([phi, phi.head(1)], ignore_index=True)
try:
merge_fees(DEMO, phi_trung)
except pd.errors.MergeError:
print(
"Xác nhận: Ngoại lệ MergeError đã được kích hoạt thành công khi bảng tra cứu trùng khóa."
)
# Kết thúc lời giảiGiải thích Cách 2
df.merge(phi, ...) là cách gọi cùng phép ghép từ đối tượng bảng trái. validate="many_to_one" viết đầy đủ cùng quy tắc với "m:1". Hàm vẫn trả ba khóa theo đề và không thêm cột vào hai bảng nguồn.
Phần minh họa bên ngoài hàm lấy giá nhân với 1 + phi_dich_vu để tính mức giá giả định gồm phí. Ví dụ phí 13% tương ứng hệ số 1.13. gia_tong.groupby(m_ket_qua["phan_khuc"]).median() dùng khóa nhóm bên ngoài để tính trung vị theo phân khúc. .round(0) chỉ làm tròn bảng minh họa sau khi tính.
pd.concat([phi, phi.head(1)], ignore_index=True) cố tình thêm lại dòng đầu để tạo khóa trùng. Gọi hàm trong try ở phần minh họa sẽ đi tới nhánh except pd.errors.MergeError. Chính hàm merge_fees vẫn để ngoại lệ thoát ra theo yêu cầu, giúp nơi gọi quyết định cách báo lỗi.
Output Cách 2
phan_khuc | Trung vị giá giả định gồm phí |
|---|---|
cao | 155250.0 |
re | 23650.0 |
trung | 62150.0 |
Bảng ghép vẫn có 12 dòng, 1 mức phí khuyết. Thử bảng phí trùng khóa phát sinh MergeError.
So sánh hai cách
Hai cách ghép có cùng kết quả và cùng cơ chế kiểm tra khóa. Cách 1 tách rõ các bước đếm để người mới quan sát. Cách 2 bổ sung tình huống sử dụng kết quả và một ca kiểm tra khóa lỗi. Ba mức phí chỉ là giả định luyện tập. Giá sau phí chưa phải doanh thu thực tế vì chưa có số đêm đặt hoặc giao dịch.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 8: Data Wrangling: Join, Combine, and Reshape.
Tài liệu chính thức pandas: Merge, join, concatenate and compare & Reshaping and pivot tables.
Hadley Wickham, Tidy Data, Journal of Statistical Software, 2014.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
IAI. Notebook thực hành và học liệu thực hành 2627-1. Các yêu cầu kỹ thuật được đối chiếu, biên tập và bổ sung lời giải cùng dữ liệu minh họa. Phần thích nghi từ học liệu IAI áp dụng giấy phép CC BY-NC-SA 4.0.