Học kì 1, năm học 2026–2027
Viện Trí tuệ nhân tạo, Trường ĐH Công nghệ, ĐHQGHN
Đã biết chọn hàng, chọn cột, lọc điều kiện và sửa bảng bằng loc.
Bài này mở rộng sang phép tính giữa các Series, tính theo nhóm và ghép nhiều bảng.
Tính chênh lệch giá khi hai bảng không cùng thứ tự.
Hai Series lưu giá qua hai lần thu thập (giả lập, USD/đêm).
| Chỉ mục | Giá |
|---|---|
| P1 | 100 |
| P2 | 80 |
| Chỉ mục | Giá |
|---|---|
| P2 | 90 |
| P1 | 110 |
gia_moi - gia_cu # Trừ hai Series
gia_moi.to_numpy() - gia_cu.to_numpy() # Bỏ nhãn, trừ hai mảng
Hai phép trừ có cho cùng giá trị không?
gia_cu = pd.Series([100, 80], index=["P1", "P2"])
gia_moi = pd.Series([90, 110], index=["P2", "P1"])
print(gia_moi - gia_cu)
P1 10
P2 10
dtype: int64
print(gia_moi.to_numpy() - gia_cu.to_numpy())
[-10 30]
Series trừ theo nhãn; mảng NumPy trừ theo vị trí.
gia_moi = pd.Series([90, 110, 70], index=["P2", "P1", "P3"])
print(gia_moi - gia_cu)
P1 10.0
P2 10.0
P3 NaN
dtype: float64
Phép trừ lấy hợp nhãn: kết quả có đủ P1, P2, P3.
P3 chưa có giá cũ nên chưa tính được.
phong = pd.DataFrame({"id": ["P1", "P2", "P3"],
"gia": [100, 80, 50]})
phong = phong.set_index("id")
print(phong)
gia
id
P1 100
P2 80
P3 50
set_index("id") dùng cột mã làm nhãn của hàng.
set_index không kiểm mã lặp: tự kiểm bằng phong.index.is_unique.
gia_cap_nhat = pd.Series([90, 110], index=["P2", "P1"])
phong["gia_moi"] = gia_cap_nhat
print(phong)
gia gia_moi
id
P1 100 110.0
P2 80 90.0
P3 50 NaN
Phép gán căn theo nhãn sẵn có của bảng, không lấy hợp nhãn như phép trừ.
Bảng phong dùng mã chỗ ở làm chỉ mục. Phí dọn dẹp (USD) được cung cấp riêng:
ma_cho_o = ["P3", "P1", "P2"]
phi = [20, 10, 30]
Thêm cột phi vào phong sao cho phí luôn khớp với mã chỗ ở, kể cả khi thứ tự hàng của bảng thay đổi.
Tra phí theo loại chỗ ở, rồi tính tiền cho mỗi hàng.
| Chỉ mục | loai | gia |
|---|---|---|
| P1 | Nguyên căn | 100 |
| P2 | Phòng riêng | 80 |
| P3 | Khác | 60 |
| loai | phí |
|---|---|
| Nguyên căn | 20 |
| Phòng riêng | 10 |
Giá theo USD/đêm, phí dọn dẹp thu một lần. Tra phí cho từng hàng:
bang["phi"] = bang["loai"].map(phi_theo_loai)
Hàng P3 mang loại “Khác”: cột phi nhận gì?
phi_theo_loai = {"Nguyên căn": 20, "Phòng riêng": 10}
bang["phi"] = bang["loai"].map(phi_theo_loai)
print(bang[["loai", "phi"]])
loai phi
P1 Nguyên căn 20.0
P2 Phòng riêng 10.0
P3 Khác NaN
map tra từng giá trị trong bảng mã. “Khác” chưa có mức phí nên nhận NaN.
Tiền ba đêm = 3 × giá mỗi đêm + phí dọn dẹp.
| Chỉ mục | gia | phi |
|---|---|---|
| P1 | 100 | 20 |
| P2 | 80 | 10 |
| P3 | 60 | NaN |
def tien_ba_dem(hang):
return 3 * hang["gia"] + hang["phi"]
print(bang.apply(tien_ba_dem, axis=1))
P1 320.0
P2 250.0
P3 NaN
dtype: float64
| Lần gọi | Giá trị trả về |
|---|---|
| P1: 3 × 100 + 20 | 320 |
| P2: 3 × 80 + 10 | 250 |
| P3: 3 × 60 + NaN | NaN |
axis=1: mỗi lần gọi, hàm nhận một hàng dưới dạng Series.
print(3 * bang["gia"] + bang["phi"])
P1 320.0
P2 250.0
P3 NaN
dtype: float64
Công thức này viết được bằng phép toán trên cột nên chưa cần apply.
Cùng một phép tính trên 18.534 hàng Santiago: tiền cho số đêm tối thiểu.
def tien_toi_thieu(hang):
return hang["price"] * hang["minimum_nights"]
%timeit listings.apply(tien_toi_thieu, axis=1)
%timeit listings["price"] * listings["minimum_nights"]
126 ms ± 958 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
123 μs ± 178 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
apply(axis=1) dựng một Series Python cho mỗi hàng: khoản chi phí Bài 3 đã chỉ ra.
Bảng dat_cho: mỗi hàng là một lượt ở; gia tính theo USD/đêm; so_dem là số đêm của lượt ở; phi là phí dọn dẹp một lần (USD).
| Chỉ mục | gia | so_dem | phi |
|---|---|---|---|
| P1 | 100 | 2 | 20 |
| P2 | 80 | 7 | 10 |
| P3 | 60 | 8 | 15 |
Thêm cột tong tính tổng tiền phòng và phí dọn dẹp cho mỗi lượt ở, trong đó khách ở từ 7 đêm được miễn phí dọn dẹp.
Ngưỡng tuyệt đối (30.000 và 100.000 CLP) và ngưỡng phân vị:
gia = listings["price"].dropna()
nguong_gia = [0, 30000, 100000, np.inf]
nhan = ["thấp", "giữa", "cao"]
cut3 = pd.cut(gia, nguong_gia, right=False, labels=nhan)
qcut3 = pd.qcut(gia, 3, labels=nhan)
print(pd.DataFrame({"cut": cut3.value_counts(),
"qcut": qcut3.value_counts()}))
cut qcut
price
thấp 1660 5913
giữa 12325 5880
cao 3703 5895
qcut tự chọn mốc 46.720 và 75.872 để ba nhóm bằng nhau.
gia12 = gia * 1.2
cut12 = pd.cut(gia12, nguong_gia, right=False, labels=nhan)
qcut12 = pd.qcut(gia12, 3, labels=nhan)
print(pd.DataFrame({"cut": cut12.value_counts(),
"qcut": qcut12.value_counts()}))
cut qcut
price
thấp 951 5913
giữa 11656 5880
cao 5081 5895
Ngưỡng tuyệt đối thấy được thị trường lên giá; ngưỡng phân vị khoá cỡ nhóm nên không thấy.
Chọn ranh giới là câu hỏi phân tích.
Từ phép tính trên từng hàng chuyển sang phép tính trên một tập hàng: so sánh giá giữa các khu, rồi so từng chỗ ở với khu của nó.
Bảng giả lập, USD/đêm. Mỗi hàng là một chỗ ở.
df = pd.DataFrame({
"id": ["P1", "P2", "P3", "P4", "P5"],
"khu": ["A", "B", "A", "B", "A"],
"gia": [100., 80., 200., 120., np.nan],
})
print(df)
id khu gia
0 P1 A 100.0
1 P2 B 80.0
2 P3 A 200.0
3 P4 B 120.0
4 P5 A NaN
| khu | Các giá được nhóm | Trung bình |
|---|---|---|
| A | 100, 200, NaN | 150 |
| B | 80, 120 | 100 |
print(df.groupby("khu")["gia"].mean())
khu
A 150.0
B 100.0
Name: gia, dtype: float64
Chia → tính → gộp (split–apply–combine): groupby chia theo khu, mean tính từng nhóm, pandas gộp thành một kết quả mỗi khu.
tk = df.groupby("khu")["gia"].agg(
trung_binh="mean", so_cho_o="size", so_gia="count")
print(tk)
trung_binh so_cho_o so_gia
khu
A 150.0 3 2
B 100.0 2 2
size đếm mọi hàng; count chỉ đếm giá không thiếu.
Bảng tk đang dùng tên khu làm chỉ mục. Đưa tên khu thành cột để lưu hoặc xử lý tiếp:
print(tk.reset_index())
khu trung_binh so_cho_o so_gia
0 A 150.0 3 2
1 B 100.0 2 2
Dùng bảng listings của Bài 4 (CLP/đêm). So sánh ba khu sau:
nguyen_can = listings.loc[
listings["room_type"] == "Entire home/apt"]
thong_ke = nguyen_can.groupby("neighbourhood")["price"].agg(
trung_vi="median", so_gia="count")
print(thong_ke.loc[["Santiago", "Providencia", "Las Condes"]])
trung_vi so_gia
neighbourhood
Santiago 49352.5 6006
Providencia 81709.0 2160
Las Condes 106129.0 2263
Chỉ xét nguyên căn để tránh gộp chung với phòng riêng.
Bảng dưới tổng hợp các giá đã biết (USD/đêm).
| Khu | Giá trung bình | Số giá đã biết |
|---|---|---|
| A | 100 | 2 |
| B | 200 | 8 |
Từ bảng trên, tính giá trung bình của toàn bộ 10 giá đã biết.
Trong mỗi khu, tính riêng giá nguyên căn và phòng riêng.
df["loai"] = ["Nguyên căn", "Phòng riêng", "Phòng riêng",
"Nguyên căn", "Nguyên căn"]
print(df.groupby(["khu", "loai"])["gia"].mean())
khu loai
A Nguyên căn 100.0
Phòng riêng 200.0
B Nguyên căn 120.0
Phòng riêng 80.0
Name: gia, dtype: float64
Mỗi cặp (khu, loai) là một nhóm.
dai = listings.groupby(
["neighbourhood", "room_type"])["price"].median()
ba_khu = dai.loc[["Santiago", "Providencia", "Las Condes"]]
print(ba_khu.head(4))
neighbourhood room_type
Santiago Entire home/apt 49352.5
Hotel room 40991.0
Private room 31250.5
Shared room 16207.5
Name: price, dtype: float64
print(ba_khu.unstack("room_type"))
room_type Entire home/apt Hotel room Private room Shared room
neighbourhood
Santiago 49352.5 40991.0 31250.5 16207.5
Providencia 81709.0 129093.0 38400.0 25000.0
Las Condes 106129.0 116213.0 40365.5 26277.0
Dạng rộng dễ đọc; dạng dài tiện để vẽ biểu đồ và ghép bảng.
Muốn biết mỗi chỗ ở cao hay thấp hơn trung bình khu của nó. Nhưng df["gia"] có chỉ mục 0, 1, 2, 3, 4 còn tb_khu có chỉ mục A, B.
tb_khu = df.groupby("khu")["gia"].mean()
print(df["gia"] - tb_khu)
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
A NaN
B NaN
Name: gia, dtype: float64
Hợp nhãn gồm cả 0…4 và A, B; không nhãn nào có mặt ở cả hai bên nên mọi giá trị đều NaN.
df["tb_khu"] = df.groupby("khu")["gia"].transform("mean")
df["chenh_lech"] = df["gia"] - df["tb_khu"]
print(df[["id", "khu", "gia", "tb_khu", "chenh_lech"]])
id khu gia tb_khu chenh_lech
0 P1 A 100.0 150.0 -50.0
1 P2 B 80.0 100.0 -20.0
2 P3 A 200.0 150.0 50.0
3 P4 B 120.0 100.0 20.0
4 P5 A NaN 150.0 NaN
Cùng mean, khác ở bước gộp: tổng hợp cho hai kết quả (mỗi nhóm một), transform cho năm kết quả (mỗi hàng một).
Chỉ phân tích các khu có từ 300 chỗ ở trở lên:
khu_dong = listings.groupby("neighbourhood").filter(
lambda nhom: len(nhom) >= 300)
print(len(khu_dong), khu_dong["neighbourhood"].nunique())
16850 8
Từ 18.534 hàng, 31 khu còn 16.850 hàng, 8 khu. Cách khác, cùng kết quả:
mask = listings.groupby("neighbourhood")["id"].transform("size") >= 300
khu_dong = listings.loc[mask]
nguyen_can = nguyen_can.copy()
nguyen_can["tv_khu"] = nguyen_can.groupby(
"neighbourhood")["price"].transform("median")
nguyen_can["lech"] = nguyen_can["price"] - nguyen_can["tv_khu"]
print(nguyen_can[["neighbourhood", "price",
"tv_khu", "lech"]].head())
neighbourhood price tv_khu lech
2 Recoleta 46776.0 52494.0 -5718.0
4 Recoleta 107044.0 52494.0 54550.0
5 Recoleta 56615.0 52494.0 4121.0
10 La Florida 61239.0 57059.0 4180.0
11 Pudahuel 63500.0 58369.0 5131.0
Chỉ mục giữ nguyên từ bảng gốc nên trừ được trực tiếp: mỗi chỗ ở so với trung vị khu của nó.
host = listings.groupby("host_id").agg(
so_cho_o=("id", "size"), trung_vi_gia=("price", "median"))
print(host.head())
so_cho_o trung_vi_gia
host_id
19611 18 70972.0
30509 1 74177.0
54585 1 130665.0
79349 2 154433.5
119648 2 32493.0
Santiago có sẵn cột calculated_host_listings_count. Tự tính lại:
n_host = listings.groupby("host_id")["id"].transform("size")
khop = n_host == listings["calculated_host_listings_count"]
print(khop.sum(), len(listings))
18534 18534
Khớp cả 18.534 hàng. Số người khác đưa, dựng lại từ dữ liệu gốc trước khi dùng.
Từ một bảng chuyển sang nhiều bảng: ghép bảng có thể làm mất hoặc nhân hàng, nên phải kiểm số hàng trước và sau.
dot_1 = pd.DataFrame({"id": ["P1", "P2"], "gia": [100, 80], "dot": 1})
dot_2 = pd.DataFrame({"id": ["P1", "P3"], "gia": [110, 70], "dot": 2})
print(pd.concat([dot_1, dot_2], ignore_index=True))
id gia dot
0 P1 100 1
1 P2 80 1
2 P1 110 2
3 P3 70 2
concat nối thêm hàng. P1 có hai lần thu thập nên xuất hiện hai lần.
| id | khu |
|---|---|
| P1 | A |
| P2 | B |
| P3 | C |
| khu | vung |
|---|---|
| A | Bắc |
| B | Nam |
Ghép theo cột khu. P3 thuộc C nhưng bảng vùng chưa có C.
m = cho_o.merge(vung, on="khu", how="left")
print(m)
id khu vung
0 P1 A Bắc
1 P2 B Nam
2 P3 C NaN
on="khu" đối chiếu theo khu; how="left" giữ mọi hàng bảng trái (left join).
Thêm indicator=True để biết mỗi chỗ ở có tìm được khu tương ứng trong bảng vung hay không.
kiem_tra = cho_o.merge(vung, on="khu", how="left",
indicator=True)
print(kiem_tra)
id khu vung _merge
0 P1 A Bắc both
1 P2 B Nam both
2 P3 C NaN left_only
both: tìm thấy khu trong bảng vùng.left_only: P3 thuộc khu C → bảng vùng không có dòng nào khớp.
Bảng vùng có khu C nhưng ô vung của C trống:
vung_thieu = pd.DataFrame({"khu": ["A", "B", "C"],
"vung": ["Bắc", "Nam", np.nan]})
print(cho_o.merge(vung_thieu, on="khu", how="left", indicator=True))
id khu vung _merge
0 P1 A Bắc both
1 P2 B Nam both
2 P3 C NaN both
Cả ba hàng đều both: khoá C khớp, chỉ thiếu giá trị. NaN bên phải không có nghĩa là không khớp khoá.
print(cho_o.merge(vung, on="khu", how="inner"))
id khu vung
0 P1 A Bắc
1 P2 B Nam
how="inner" (inner join) chỉ giữ cặp khớp khoá: P3 biến mất. inner là mặc định, quên how là mất hàng.
vung_lap = pd.DataFrame({"khu": ["A", "A", "B"],
"vung": ["Bắc", "Trung tâm", "Nam"]})
print(cho_o.merge(vung_lap, on="khu", how="left"))
id khu vung
0 P1 A Bắc
1 P1 A Trung tâm
2 P2 B Nam
3 P3 C NaN
P1 khớp hai hàng mang khoá A nên xuất hiện hai lần.
many_to_one: nhiều chỗ ở một khu; bảng vùng mỗi khu một dòng.
cho_o.merge(vung_lap, on="khu", how="left",
validate="many_to_one")
MergeError: Merge keys are not unique in right dataset;
not a many-to-one merge
Khu A lặp nên pandas dừng, thay vì lặng lẽ nhân đôi P1.
mo_rong = listings.merge(host, on="host_id", how="left",
validate="many_to_one")
assert len(mo_rong) == len(listings)
print(len(listings), "->", len(mo_rong))
18534 -> 18534
validate: mỗi chỗ ở khớp đúng một host. assert: số hàng phải không đổi, lần chạy sau tự báo lỗi.
| id | khu |
|---|---|
| P1 | A |
| P2 | A |
| P3 | B |
| khu | vung |
|---|---|
| A | Bắc |
| A | Trung tâm |
Viết bảng kết quả của lệnh sau:
trai.merge(phai, on="khu", how="left")
Một chuỗi trả bảng mới: chạy lại bao nhiêu lần cũng ra một kết quả.
Đổi giá sang nghìn CLP cho dễ đọc, gán đè lên cột price:
co_gia = listings.loc[listings["price"].notna()]
co_gia["price"] = co_gia["price"] / 1000
print(co_gia["price"].median())
59.0
co_gia["price"] = co_gia["price"] / 1000 # chạy lại đúng dòng trên
print(co_gia["price"].median())
0.059
Gán đè lên cột cũ: mỗi lần chạy lại, giá bị chia thêm cho 1000.
assign(gia_nghin=...) trả về một bảng mới có thêm cột gia_nghin; bảng gốc giữ nguyên.
co_gia = listings.loc[listings["price"].notna()]
moi = co_gia.assign(gia_nghin=co_gia["price"] / 1000)
print(moi[["price", "gia_nghin"]].head(3))
price gia_nghin
0 45647.0 45.647
1 19856.0 19.856
2 46776.0 46.776
print(co_gia["price"].median())
59000.0
co_gia vẫn giữ giá CLP, nên chạy lại ô trên bao nhiêu lần thì moi vẫn như vậy.
Mỗi bước là một hàm: nhận bảng, trả một cột.
def chia_phan_khuc(d):
return pd.cut(d["price"], nguong_gia, right=False, labels=nhan)
def dem_host(d):
return d.groupby("host_id")["id"].transform("size")
Giá trị là hàm thì assign tự gọi hàm với bảng: cột phan_khuc là kết quả của chia_phan_khuc(co_gia).
co_gia = listings.loc[listings["price"].notna()]
mo_rong = co_gia.assign(phan_khuc=chia_phan_khuc, n_host=dem_host)
print(co_gia.shape, mo_rong.shape)
(17688, 19) (17688, 21)
print("phan_khuc" in listings.columns)
False
Bảng mới có thêm hai cột. listings không đổi.
ket_qua = (listings
.loc[listings["price"].notna()]
.assign(phan_khuc=chia_phan_khuc, n_host=dem_host)
.groupby("phan_khuc", observed=True)
.agg(so_cho_o=("id", "size"),
trung_vi_gia=("price", "median"),
tv_n_host=("n_host", "median")))
print(ket_qua)
so_cho_o trung_vi_gia tv_n_host
phan_khuc
thấp 1660 23617.5 1.0
giữa 12325 54776.0 2.0
cao 3703 145318.0 3.0
Lọc → thêm cột → nhóm → tổng hợp. Không sửa listings, nên chạy lại vẫn ra một kết quả.
transform trả kết quả cho từng hàng.