Giao diện
1. Giới thiệu cấu trúc dữ liệu trong pandas
Bạn nhận được bảng dữ liệu giá phòng cho thuê gồm nhiều cột: Mã căn hộ, loại phòng, số đêm lưu trú, đơn giá mỗi đêm và phí dọn dẹp. Một số căn hộ chưa ghi nhận phí dọn phòng hoặc để trống số đêm. Nếu dùng mảng hai chiều NumPy, việc kết hợp đồng thời chuỗi ký tự tên loại phòng và số thực đơn giá sẽ buộc toàn bộ mảng chuyển thành kiểu chuỗi hoặc đối tượng rời rạc, làm mất đi khả năng tính toán số học nhanh. Thư viện pandas giải quyết vấn đề này bằng cách cung cấp cấu trúc bảng hai chiều DataFrame, nơi mỗi cột lưu trữ một kiểu dữ liệu riêng biệt nhưng các hàng được kết nối chặt chẽ qua một hệ thống chỉ mục nhãn đồng nhất.
1.1. Cấu trúc Series: Mảng một chiều có nhãn
Series là đối tượng mảng một chiều gồm hai thành phần gắn kết chặt chẽ:
- Mảng giá trị (values): Vùng đệm dữ liệu số học hoặc đối tượng bên dưới, có kiểu dữ liệu
dtypeđồng nhất. - Chỉ mục nhãn (index): Mảng các nhãn định danh đi kèm từng phần tử.
python
import pandas as pd
import numpy as np
# Khởi tạo Series với chỉ mục mặc định (0, 1, 2, ...)
s1 = pd.Series([4.5, 7.2, -5.3, 3.0])
# Khởi tạo Series với chỉ mục nhãn tùy biến
s2 = pd.Series([100, 80, 60], index=["P01", "P02", "P03"])
print(s2)
# P01 100
# P02 80
# P03 60
# dtype: int64Khác với danh sách Python, Series cho phép truy xuất phần tử theo nhãn tương tự từ điển: s2["P01"]. Đồng thời, Series hỗ trợ trọn vẹn các phép toán vector hóa của NumPy:
python
s_gap_doi = s2 * 2
s_loc = s2[s2 >= 80]Mọi phép toán số học đều tự động bảo lưu liên kết nhãn với giá trị tương ứng.
1.2. Cấu trúc DataFrame: Bảng dữ liệu hai chiều
DataFrame biểu diễn một bảng dữ liệu hai chiều có cấu trúc, gồm tập hợp các cột có tên nhãn cụ thể, trong đó mỗi cột có thể chứa một kiểu dữ liệu riêng biệt.
Khởi tạo DataFrame
DataFrame có thể được xây dựng từ nhiều nguồn dữ liệu:
- Từ một từ điển chứa các danh sách có cùng độ dài:
python
du_lieu = {
"khu_vuc": ["Bắc", "Bắc", "Nam", "Nam"],
"nam": [2024, 2025, 2024, 2025],
"doanh_thu": [150.5, 180.2, 210.0, 245.8]
}
df = pd.DataFrame(du_lieu, index=["HN1", "HN2", "SG1", "SG2"])- Từ từ điển lồng nhau: Các khóa ngoài trở thành tên cột, các khóa trong trở thành nhãn hàng (index).
Thuộc tính và thao tác cơ bản
df.shape: Tuple trả về số hàng và số cột(so_hang, so_cot).df.columns: Đối tượng Index chứa danh sách tên các cột.df.index: Đối tượng Index chứa danh sách nhãn các hàng.df.dtypes: Series hiển thị kiểu dữ liệu của từng cột.df.head(n)vàdf.tail(n): Xem nhanhdòng đầu tiên hoặc dòng cuối cùng của bảng.
Trích xuất một cột từ DataFrame bằng cú pháp ngoặc vuông df["doanh_thu"] hoặc truy cập thuộc tính df.doanh_thu sẽ trả về một đối tượng Series. Thêm một cột mới vào bảng được thực hiện bằng phép gán:
python
df["thue"] = df["doanh_thu"] * 0.1Xóa một cột khỏi bảng sử dụng câu lệnh del df["thue"] hoặc phương thức .drop().
1.3. Đối tượng chỉ mục Index
Các đối tượng Index trong pandas chịu trách nhiệm lưu trữ nhãn trục (hàng và cột) và các siêu dữ liệu khác.
Tính bất biến của Index
Các đối tượng Index mang tính bất biến (immutable). Lập trình viên không thể chỉnh sửa riêng lẻ một nhãn bên trong Index:
python
idx = df.index
idx[0] = "HN_MOI" # TypeError: Index does not support mutable operationsTính bất biến này giúp bảo đảm an toàn dữ liệu, cho phép chia sẻ cùng một đối tượng Index giữa nhiều cấu trúc DataFrame và Series mà không sợ bị biến đổi ngoài tầm kiểm soát.
Đối tượng Index hỗ trợ đầy đủ các phép toán đại số tập hợp:
idx1.union(idx2): Tập hợp hợp.idx1.intersection(idx2): Tập hợp giao.idx1.difference(idx2): Tập hợp hiệu.idx.is_unique: Kiểm tra xem các nhãn chỉ mục có duy nhất hay không.
2. Các chức năng thiết yếu của pandas
2.1. Tái lập chỉ mục
Phương thức .reindex() tạo ra một đối tượng mới với dữ liệu được sắp xếp lại theo một chỉ mục nhãn mới. Nếu một nhãn mới không có trong dữ liệu ban đầu, pandas sẽ điền giá trị khuyết thiếu NaN:
python
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
s_moi = s.reindex(["a", "b", "c", "d"], fill_value=0)
# a 10
# b 20
# c 30
# d 0Trong dữ liệu chuỗi thời gian, ta có thể nội suy các giá trị thiếu khi đổi chỉ mục bằng tham số method:
method="ffill"(forward fill): Điền tiếp giá trị của quan sát trước đó về phía sau.method="bfill"(backward fill): Lấy giá trị của quan sát liền sau điền ngược về trước.
2.2. Loại bỏ dòng và cột với drop
Phương thức .drop() loại bỏ các nhãn chỉ định khỏi một trục và trả về một đối tượng mới:
python
# Loại bỏ hàng theo nhãn (axis=0 mặc định)
df_bot_hang = df.drop(["HN1", "SG1"])
# Loại bỏ cột theo tên (axis=1 hoặc axis="columns")
df_bot_cot = df.drop(columns=["nam"])2.3. Chỉ mục, lựa chọn và sàng lọc với loc và iloc
Để tránh mơ hồ giữa việc chọn theo nhãn danh nghĩa hay chọn theo vị trí số nguyên, pandas cung cấp hai toán tử chỉ mục chuyên dụng:
1. Chọn theo nhãn với .loc
Toán tử .loc[hang_nhan, cot_nhan] tiếp nhận nhãn danh nghĩa:
python
# Chọn một ô đơn lẻ
gia_tri = df.loc["HN1", "doanh_thu"]
# Chọn một tập hợp hàng và cột
bang_con = df.loc[["HN1", "SG1"], ["khu_vuc", "doanh_thu"]]
# Cắt lát theo nhãn: BAO GỒM CẢ ĐIỂM KẾT THÚC
lat_cat_loc = df.loc["HN1":"SG1", "khu_vuc":"doanh_thu"]2. Chọn theo vị trí số nguyên với .iloc
Toán tử .iloc[hang_vi_tri, cot_vi_tri] tiếp nhận chỉ số nguyên từ
python
# Chọn hàng 0, cột 2
gia_tri = df.iloc[0, 2]
# Cắt lát theo vị trí: KHÔNG BAO GỒM ĐIỂM KẾT THÚC (chuẩn Python)
lat_cat_iloc = df.iloc[0:2, 1:3]Cạm bẫy chỉ mục số nguyên
Xét Series có nhãn nguyên nhưng thứ tự nhãn khác vị trí:
python
s = pd.Series([10, 20, 30], index=[2, 0, 1])
assert s[1] == s.loc[1] == 30
assert s.iloc[1] == 20Trong trường hợp này, s[1] tìm nhãn 1 chứ không lấy phần tử thứ hai. s[-1] báo KeyError vì không có nhãn -1. Viết .loc và .iloc giúp thể hiện rõ ý định chọn theo nhãn hay vị trí.
Cạm bẫy gán chuỗi và cơ chế Copy-on-Write
Một thao tác dễ gây lỗi logic là viết câu lệnh gán hai lần ngoặc vuông (chained indexing):
pycon
>>> phong_demo["gia"][phong_demo["gia"] < 50] = 50Với phong_demo là DataFrame có cột gia, lần chọn đầu lấy một Series trung gian. Trong pandas 3 dùng Copy-on-Write, phép gán chuỗi này không cập nhật bảng gốc và phát cảnh báo thuộc loại ChainedAssignmentError. Đừng hiểu tên cảnh báo là bảo đảm chương trình luôn dừng bằng ngoại lệ, vì điều đó còn tùy cấu hình xử lý warning.
Gán rõ bảng đích trong một bước
Một cách cập nhật là dùng .loc với cả điều kiện hàng và tên cột:
python
phong_demo = pd.DataFrame({"gia": [40.0, 70.0]})
phong_demo.loc[phong_demo["gia"] < 50, "gia"] = 50
assert phong_demo["gia"].tolist() == [50.0, 70.0]2.4. Số học và căn chỉnh dữ liệu tự động
Khi thực hiện các phép tính số học giữa hai đối tượng Series hoặc DataFrame, pandas tự động căn chỉnh dữ liệu theo nhãn:
python
s1 = pd.Series([10, 20], index=["a", "b"])
s2 = pd.Series([30, 40], index=["b", "c"])
ket_qua_cong = s1 + s2
assert ket_qua_cong.loc["b"] == 50
assert ket_qua_cong.loc[["a", "c"]].isna().all()Phép cộng
- Nhãn
"a"chỉ có ở, thiếu ở kết quả là NaN. - Nhãn
"b"có ở cả hai bên:. - Nhãn
"c"chỉ có ở, thiếu ở kết quả là NaN.
Muốn thay thế các giá trị khuyết thiếu bằng một hằng số khi tính toán, ta sử dụng các phương thức số học có tham số fill_value:
python
s_tong = s1.add(s2, fill_value=0)
# a 10.0
# b 50.0
# c 40.0Phép toán giữa DataFrame và Series (Broadcasting)
Theo mặc định, phép toán giữa DataFrame và Series khớp nhãn Series với tên cột rồi dùng lại các giá trị đó cho mỗi hàng. Muốn khớp với nhãn hàng, hãy chỉ định axis="index" hoặc axis=0:
python
bang_so = pd.DataFrame({"A": [10, 20], "B": [30, 40]}, index=["r1", "r2"])
s_theo_hang = pd.Series([1, 2], index=["r1", "r2"])
df_tru = bang_so.sub(s_theo_hang, axis="index")
np.testing.assert_array_equal(df_tru.to_numpy(), [[9, 29], [18, 38]])2.5. Áp dụng hàm và ánh xạ
df.apply(ham, axis=0): Áp dụng một hàm số lên từng cột (hoặc từng hàng nếuaxis=1). Thường dùng cho các hàm tổng hợp hoặc biến đổi phức tạp.ser.map(anh_xa): Ánh xạ từng phần tử của Series thông qua một từ điển tra cứu hoặc một hàm một đối số.df.map(ham): Áp dụng một hàm lên từng ô đơn lẻ của toàn bộ DataFrame (thay thế phương thứcapplymapcũ).
python
# Tra cứu phí theo loại phòng bằng map
bang_phi = {"Nguyên căn": 20, "Phòng riêng": 10}
phong_phi = pd.DataFrame({"loai": ["Nguyên căn", "Phòng riêng"]})
phong_phi["phi"] = phong_phi["loai"].map(bang_phi)
assert phong_phi["phi"].tolist() == [20, 10]2.6. Sắp xếp và xếp hạng
- Sắp xếp theo chỉ mục nhãn:
df.sort_index(ascending=True). - Sắp xếp theo giá trị cột:
df.sort_values(by="doanh_thu", ascending=False). - Xếp hạng dữ liệu: Phương thức
.rank()gán thứ hạng từđến cho các phần tử. Khi các giá trị bằng nhau, tham số methodquyết định cách giải quyết hòa vị:"average"(mặc định): Lấy trung bình cộng các thứ hạng hòa (ví dụ hai số cùng nhỏ nhất chia nhau hạng 1 và 2 sẽ nhận hạng). "min": Cùng nhận thứ hạng nhỏ nhất."max": Cùng nhận thứ hạng lớn nhất."first": Xếp hạng theo thứ tự xuất hiện trước trong dữ liệu."dense": Tăng thứ hạng liền kề mà không bỏ qua bậc (hạng 1, hạng 1, tiếp theo là hạng 2).
3. Thống kê mô tả và tóm tắt dữ liệu
3.1. Các phương thức thống kê mô tả
pandas cung cấp đầy đủ các phương thức thống kê tổng hợp trên DataFrame:
df.sum(axis=0): Tính tổng theo từng cột.df.mean(axis=1): Tính trung bình theo từng hàng.df.describe(): Bảng tóm tắt toàn diện gồm số lượng đếm, trung bình, độ lệch chuẩn, giá trị nhỏ nhất, các phân vị, (trung vị), , và giá trị lớn nhất.
Tham số skipna và min_count
Theo mặc định, các phương thức thống kê của pandas tự động bỏ qua các giá trị khuyết thiếu (skipna=True). Tuy nhiên, cần đặc biệt lưu ý: Một Series toàn giá trị NaN khi gọi .sum() mặc định sẽ trả về min_count=1:
python
s_trong = pd.Series([np.nan, np.nan])
print(s_trong.sum()) # 0.0 (Nguy cơ gây hiểu lầm)
print(s_trong.sum(min_count=1)) # NaN (Phản ánh đúng dữ liệu khuyết)3.2. Tương quan và hiệp phương sai
s1.corr(s2): Tính hệ số tương quan Pearson giữa hai Series.df.corr(): Ma trận tương quan giữa tất cả các cặp cột số học.df.corrwith(s1): Tính tương quan giữa từng cột của DataFrame với một Series chỉ định.
3.3. Giá trị duy nhất, đếm tần suất và kiểm tra thành viên
ser.unique(): Trả về một mảng NumPy chứa các giá trị phân biệt theo thứ tự xuất hiện.ser.value_counts(): Trả về Series thống kê tần suất xuất hiện của từng giá trị, sắp xếp giảm dần.ser.isin(danh_sach): Trả về Series boolean kiểm tra từng ô có thuộc danh sách giá trị cho trước hay không.
Tài liệu tham khảo
Wes McKinney. Python for Data Analysis: Data Wrangling with pandas, NumPy, and Jupyter, tái bản lần 3. O'Reilly Media, 2022. Chương 5: Getting Started with pandas.
The pandas Development Team. pandas documentation: Indexing and selecting data & Copy-on-Write. https://pandas.pydata.org/docs/user_guide/indexing.html
Viện Trí tuệ Nhân tạo, Trường Đại học Công nghệ, ĐHQGHN. Học liệu môn học Lập trình xử lý dữ liệu: Bài 4 - Làm quen với pandas và Bài 5 - Series & DataFrame chuyên sâu. Các bài tập thực hành trong bài giảng được biên soạn và chuẩn hóa dựa trên học liệu mở của IAI 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 thực hành
Bài tập 1: Chọn chỗ ở theo ngân sách và ngưỡng số đêm
Bảng dữ liệu giả lập về ba chỗ ở với giá thuê mỗi đêm và phí dọn dẹp một lần duy nhất cho mỗi lượt ở (đơn vị USD):
| Chỗ ở | Giá mỗi đêm | Phí dọn dẹp |
|---|---|---|
| a | 100 | 0 |
| b | 80 | 100 |
| c | 60 | 300 |
- Tạo DataFrame
cho_obiểu diễn bảng trên với chỉ mục là tên các chỗ ở["a", "b", "c"]. - Sử dụng phép toán trên Series để tính tổng số tiền cho một chuyến đi kéo dài 2 đêm và 7 đêm. Trong mỗi trường hợp, chỗ ở nào có tổng chi phí thấp nhất?
- Từ bao nhiêu đêm trở lên thì chỗ ở
csẽ có tổng chi phí rẻ hơn cảavàb? Hãy lập luận bằng bất đẳng thức và kiểm tra lại bằng mã nguồn Python.
Lời giải
- Tạo DataFrame:
python
import pandas as pd
cho_o = pd.DataFrame({
"gia_dem": [100, 80, 60],
"phi_don": [0, 100, 300]
}, index=["a", "b", "c"])- Với mỗi chỗ ở, gọi p là giá mỗi đêm và f là phí dọn dẹp một lần. Tổng tiền cho n đêm là:
- Với chuyến đi 2 đêm (
): - Chỗ ở
a:USD. - Chỗ ở
b:USD. - Chỗ ở
c:USD. - Lựa chọn rẻ nhất là chỗ ở
a.
- Chỗ ở
- Với chuyến đi 7 đêm (
): - Chỗ ở
a:USD. - Chỗ ở
b:USD. - Chỗ ở
c:USD. - Lựa chọn rẻ nhất là chỗ ở
b.
- Chỗ ở
- Với chuyến đi 2 đêm (
- Xác định số đêm tối thiểu để
crẻ hơn cảavàb: Gọilà số đêm lưu trú ( là số nguyên dương): - Để
crẻ hơna: - Để
crẻ hơnb:
. Vì số đêm là số nguyên nên số đêm lưu trú tối thiểu để ctrở thành phương án tiết kiệm nhất là 11 đêm. Kiểm tra số học:- Tại
: a, b, c( bvàcbằng giá nhau). - Tại
: a, b, c( crẻ nhất).
- Để
Bài tập 2: Phân biệt biến động giá và thay đổi tập mẫu quan sát
Một nghiên cứu theo dõi giá thuê của ba chỗ ở qua hai đợt thu thập dữ liệu (đơn vị USD/đêm):
| Chỗ ở | Lần 1 | Lần 2 |
|---|---|---|
| a | 100 | 110 |
| b | 200 | 210 |
| c | 300 | Chưa có giá (khuyết thiếu) |
- Tạo DataFrame
df_giavà tính giá trung bình của các giá đã biết ở mỗi lần thu thập. - Tính lại giá trung bình ở cả hai lần khi chỉ giới hạn khảo sát trên tập chỗ ở có mặt ở cả hai đợt (chỗ ở
avàb). - Vì sao cách tính thứ nhất lại cho thấy giá trung bình của thị trường bị giảm, trong khi cách tính thứ hai cho thấy giá thực tế đang tăng? Bài học rút ra khi phân tích dữ liệu khuyết thiếu là gì?
Lời giải
python
import numpy as np
import pandas as pd
df_gia = pd.DataFrame({
"lan_1": [100.0, 200.0, 300.0],
"lan_2": [110.0, 210.0, np.nan]
}, index=["a", "b", "c"])
# 1. Trung bình trên các giá đã biết
tb_lan1 = df_gia["lan_1"].mean() # (100 + 200 + 300) / 3 = 200.0
tb_lan2 = df_gia["lan_2"].mean() # (110 + 210) / 2 = 160.0
# 2. Trung bình khi chỉ xét các chỗ ở có mặt ở cả hai lần (a và b)
tb_chung_lan1 = df_gia.loc[["a", "b"], "lan_1"].mean() # (100 + 200) / 2 = 150.0
tb_chung_lan2 = df_gia.loc[["a", "b"], "lan_2"].mean() # (110 + 210) / 2 = 160.0
print(f"Lần 1 toàn bộ: {tb_lan1}, Lần 2 toàn bộ: {tb_lan2}")
print(f"Lần 1 (a, b): {tb_chung_lan1}, Lần 2 (a, b): {tb_chung_lan2}")- Giải thích bản chất:
- Ở cách tính thứ nhất, giá trung bình giảm từ
xuống USD không phải vì giá phòng hạ nhiệt, mà do sự thay đổi thành phần của tập mẫu quan sát. Ở lần 1, mẫu có mặt căn hộ cao cấp cvới giáUSD kéo trung bình lên cao. Sang lần 2, căn hộ cbị khuyết thiếu dữ liệu, mẫu chỉ còn lại hai căn hộ giá thấp hơn. - Ở cách tính thứ hai trên cùng một tập đối tượng so sánh nhất quán (
avàb), giá phòng thực tế tăng từlên USD (mỗi phòng đều tăng đúng USD). - Bài học kinh nghiệm: Khi so sánh các chỉ số thống kê tổng hợp qua thời gian, phải luôn kiểm tra tính nhất quán của tập mẫu quan sát. Dữ liệu khuyết thiếu không ngẫu nhiên có thể bóp méo hoàn toàn kết luận kinh tế.
- Ở cách tính thứ nhất, giá trung bình giảm từ
Bài tập 3: Lọc dữ liệu theo ngân sách và điều kiện kết hợp
Cho bảng dữ liệu lua_chon về các phòng cho thuê:
python
lua_chon = pd.DataFrame({
"loai": ["Nguyên căn", "Phòng riêng", "Nguyên căn", "Phòng riêng"],
"gia": [90, 70, 120, 60]
}, index=["a", "b", "c", "d"])Khách hàng đặt ra yêu cầu ngân sách: Nếu là phòng loại "Nguyên căn" thì giá phải
- Dự đoán bằng mắt những hàng nào thỏa mãn yêu cầu ngân sách trên.
- Viết một mặt nạ boolean kết hợp toán tử logic để lọc ra các hàng thỏa mãn, chỉ hiển thị hai cột
loaivàgia.
Lời giải
- Phân tích từng dòng dữ liệu:
- Hàng
a: Loại "Nguyên căn", giáThỏa mãn. - Hàng
b: Loại "Phòng riêng", giáKhông thỏa mãn (mặc dù nhưng đây là phòng riêng). - Hàng
c: Loại "Nguyên căn", giáKhông thỏa mãn. - Hàng
d: Loại "Phòng riêng", giáThỏa mãn (nằm đúng trên biên ngân sách). - Danh sách hàng được chọn gồm
avàd.
- Hàng
- Mã lệnh lọc với toán tử
.loc:
python
mask = ((lua_chon["loai"] == "Nguyên căn") & (lua_chon["gia"] <= 100)) | \
((lua_chon["loai"] == "Phòng riêng") & (lua_chon["gia"] <= 60))
ket_qua = lua_chon.loc[mask, ["loai", "gia"]]
print(ket_qua)Kết quả trả về DataFrame gồm hai dòng a và d với đầy đủ hai cột loai và gia.
Bài tập 4: Sửa giá phòng và cạm bẫy phụ thuộc cột
Cho bảng dữ liệu ban đầu:
python
df = pd.DataFrame({
"loai": ["Nguyên căn", "Phòng riêng", "Nguyên căn"],
"gia": [100.0, 50.0, np.nan]
}, index=["a", "b", "c"])Yêu cầu: Tạo bảng báo giá mới bao_gia từ df. Áp dụng chính sách giảm giá ba_dem = gia * 3. Giữ nguyên bảng df ban đầu không bị biến đổi. Một lập trình viên viết đoạn mã sau:
python
bao_gia = df.copy()
bao_gia["ba_dem"] = bao_gia["gia"] * 3
mask = bao_gia["loai"] == "Nguyên căn"
gia_nguyen_can = bao_gia.loc[mask, "gia"]
gia_nguyen_can *= 0.9- Đoạn mã trên cho giá trị cột
ba_demcủa phòngabằng bao nhiêu? Theo đúng yêu cầu bài toán, con số này phải bằng bao nhiêu? - Chỉ ra hai lỗi logic trong đoạn mã trên và viết lại đoạn mã chuẩn xác.
Lời giải
- Đoạn mã trên cho giá trị
ba_demcủa phòngalà. Tuy nhiên, theo đúng yêu cầu nghiệp vụ, giá phòng asau khi giảmcòn USD, do đó tiền ba đêm phải là USD. - Hai lỗi logic:
- Lỗi thứ tự tính toán: Đoạn mã tính toán cột
ba_demtrước khi thực hiện giảm giá. Các cột trong DataFrame lưu trữ giá trị tĩnh, không tự động cập nhật công thức như bảng tính Excel khi cột thành phần thay đổi. - Lỗi gán trên đối tượng trích xuất: Dòng
gia_nguyen_can = bao_gia.loc[mask, "gia"]trích xuất một Series riêng biệt. Phép toángia_nguyen_can *= 0.9chỉ biến đổi trên Series này mà không cập nhật ngược lại vào cộtgiacủabao_gia(do cơ chế bảo vệ Copy-on-Write).
- Lỗi thứ tự tính toán: Đoạn mã tính toán cột
- Đoạn mã sửa lại chuẩn xác:
python
bao_gia = df.copy()
# Bước 1: Giảm 10% giá phòng nguyên căn trực tiếp trên bảng
mask = bao_gia["loai"] == "Nguyên căn"
bao_gia.loc[mask, "gia"] = bao_gia.loc[mask, "gia"] * 0.9
# Bước 2: Tính toán cột tiền ba đêm sau khi giá đã được cập nhật
bao_gia["ba_dem"] = bao_gia["gia"] * 3
print("Bảng báo giá mới:")
print(bao_gia)
print("\nBảng gốc df vẫn được bảo toàn:")
print(df)Kết quả cho thấy bao_gia.loc["a", "gia"] bao_gia.loc["a", "ba_dem"] b giữ nguyên c giữ nguyên NaN. Bảng df gốc không bị ảnh hưởng.
Bài tập 5: Bảo toàn mã định danh khi lưu trữ tệp CSV
Bảng dữ liệu df sử dụng các ký tự "a", "b", "c" làm nhãn chỉ mục hàng (index). Một kỹ sư xuất dữ liệu gửi cho đối tác bằng lệnh:
python
bang_gui = df[["gia"]]
bang_gui.to_csv("bang_gui.csv", index=False)- Khi đối tác mở tệp
bang_gui.csv, họ có biết từng giá tiền thuộc về mã chỗ ở nào không? Hãy mô tả nội dung của tệp CSV được sinh ra. - Hãy sửa lại cách lưu trữ để bảo đảm người nhận luôn đọc được đầy đủ cả mã chỗ ở lẫn giá tiền tương ứng.
Lời giải
Với index=False, tệp chỉ giữ cột giá mà không có các mã a, b, c. Vì người nhận không có bảng gốc, vị trí hàng không đủ để xác định mỗi giá thuộc chỗ ở nào.
Một cách là giữ chỉ mục và đặt tên cột mã khi xuất:
python
from tempfile import TemporaryDirectory
from pathlib import Path
df_gui = pd.DataFrame({"gia":[100.0,50.0,np.nan]},index=["a","b","c"])
with TemporaryDirectory() as thu_muc:
tep = Path(thu_muc)/"bang_gui.csv"
df_gui.to_csv(tep,index=True,index_label="ma_cho_o")
doc_lai = pd.read_csv(tep,index_col="ma_cho_o")
assert doc_lai.index.tolist() == ["a","b","c"]
np.testing.assert_allclose(doc_lai["gia"],[100,50,np.nan],equal_nan=True)Cách khác là đưa mã thành cột dữ liệu trước khi dùng index=False:
python
bang_gui_chuan = df_gui.rename_axis("ma_cho_o").reset_index()
assert bang_gui_chuan.columns.tolist() == ["ma_cho_o","gia"]Khi lưu bảng thứ hai, dùng bang_gui_chuan.to_csv(tep, index=False) tại đường dẫn xuất mà bạn đã chọn. Hãy đọc lại tệp và kiểm các cặp mã–giá, không chỉ kiểm số hàng.
Bài tập 6: Gán cột khớp theo nhãn chỉ mục độc lập với thứ tự
Bảng phong quản lý danh sách các phòng với mã chỗ ở làm chỉ mục. Thông tin về phí dọn dẹp được đối tác cung cấp riêng dưới dạng hai danh sách Python độc lập có thứ tự bị xáo trộn:
python
phong = pd.DataFrame({"gia": [100, 80, 60]}, index=["P1", "P2", "P3"])
ma_cho_o = ["P3", "P1", "P2"]
phi = [20, 10, 30]Hãy thêm cột phi vào bảng phong sao cho giá trị phí luôn khớp chính xác với từng mã chỗ ở, kể cả khi thứ tự các hàng trong bảng phong bị thay đổi.
Lời giải
Nếu gán trực tiếp danh sách phi bằng câu lệnh phong["phi"] = phi, pandas sẽ gán theo thứ tự vị trí vật lý từ trên xuống dưới, khiến P1 nhận P2 nhận P3 nhận
Giải pháp chuẩn xác là chuyển đổi danh sách thành một đối tượng Series có gắn kèm chỉ mục nhãn ma_cho_o trước khi gán:
python
phong["phi"] = pd.Series(phi, index=ma_cho_o)
print(phong)Kết quả:
text
gia phi
P1 100 10
P2 80 30
P3 60 20pandas tự động căn chỉnh theo nhãn: Mã P1 nhận đúng phí P2 nhận P3 nhận phong bằng lệnh phong = phong.loc[["P3", "P1", "P2"]], phép gán vẫn duy trì sự toàn vẹn dữ liệu.
Bài tập 7: Tính toán chi phí có điều kiện và miễn phí dọn dẹp
Cho bảng dat_cho chứa thông tin về các lượt khách đặt phòng:
python
dat_cho = pd.DataFrame({
"gia": [100, 80, 60],
"so_dem": [2, 7, 8],
"phi": [20, 10, 15]
}, index=["P1", "P2", "P3"])Chính sách ưu đãi của hệ thống: Khách lưu trú từ tong (gồm tiền phòng và phí dọn dẹp thực tế) cho từng lượt khách và thêm vào bảng dat_cho.
Lời giải
Ta có thể thực hiện theo hai phương pháp chuẩn mực:
Cách 1: Lập trình vector hóa với .where() (Khuyến nghị vì tối ưu tốc độ)
python
# Tính phí dọn dẹp sau miễn giảm: Nếu so_dem < 7 giữ nguyên phi, ngược lại bằng 0
phi_thuc_te = dat_cho["phi"].where(dat_cho["so_dem"] < 7, 0)
dat_cho["tong"] = dat_cho["gia"] * dat_cho["so_dem"] + phi_thuc_te
print(dat_cho)Cách 2: Áp dụng hàm theo từng hàng với .apply()
python
def tinh_tien_luot_o(hang):
phi_ap_dung = 0 if hang["so_dem"] >= 7 else hang["phi"]
return hang["gia"] * hang["so_dem"] + phi_ap_dung
dat_cho["tong"] = dat_cho.apply(tinh_tien_luot_o, axis=1)Kết quả tính toán:
- Lượt
P1(2 đêm): USD. - Lượt
P2(7 đêm, miễn phí): USD. - Lượt
P3(8 đêm, miễn phí): USD.
Lab 4: Lập hồ sơ phân tích một quận bằng pandas
Một bảng niêm yết chứa nhiều quận, loại phòng và mức giá. Ta sẽ lọc riêng một quận, lập hồ sơ giá và cơ cấu phòng, rồi kiểm tra ảnh hưởng của các ô thiếu. Mỗi phép tính đều được đối chiếu trên dữ liệu nhỏ trước khi dùng với snapshot đầy đủ.
1. Mục tiêu và bối cảnh thực tế
Xét bài toán thực tế: Một nhà đầu tư cần bản hồ sơ cô đọng về thị trường quận Ñuñoa thuộc vùng đô thị Santiago de Chile. Bản hồ sơ cần giải đáp: Quy mô nguồn cung của quận là bao nhiêu, giá thuê so với mặt bằng toàn thành phố cao hay thấp, cơ cấu phòng tập trung ở loại hình nào, và dữ liệu có điểm dị biệt hay giá trị khuyết nào ảnh hưởng đến quyết định đầu tư.
Mục tiêu kỹ thuật cốt lõi
- Thói quen 5 bước khám phá dữ liệu: Thực thi các lệnh tổng quan ban đầu và rút trích trực tiếp số liệu định lượng từ kết quả đầu ra, thay vì chỉ quan sát thụ động.
- Trích xuất và lọc đa điều kiện: Phân biệt chính xác giữa
df["cột"](trả về Series),df[["cột1", "cột2"]](trả về DataFrame),df[mask](lọc hàng) vàdf.loc[mask, "cột"](truy xuất đúng ô cần thao tác), đồng thời kết hợp nhiều điều kiện logic bằng toán tử bitwise&và phương thứcnsmallest. - Cập nhật dữ liệu an toàn và đồng bộ cột phụ thuộc: Sử dụng
.loc[hàng, cột]để sửa đổi bảng dữ liệu an toàn, hiểu rõ nguyên lý một cột trong DataFrame chỉ lưu trữ giá trị tĩnh đã tính toán chứ không tự động cập nhật như ô tính bảng tính. - Phân tích tác động của dữ liệu khuyết (NaN): Tính toán tỷ lệ dựa trên giá trị boolean và phân biệt rõ vai trò của mẫu số, tránh ngộ nhận khi dữ liệu chứa giá trị thiếu.
- Đóng gói xuất bản và kiểm chứng bất biến: Xuất kết quả phân tích ra tệp CSV chuẩn hóa, đọc lại tệp và kiểm tra tính toàn vẹn dữ liệu thông qua các phép kiểm bất biến độc lập.
2. Dữ liệu thực hành và thiết lập môi trường
Để bảo đảm toàn bộ quy trình thực hành có thể chạy độc lập, bài thực hành sử dụng bộ dữ liệu mẫu gồm 12 dòng (DEMO, đơn vị giá: CLP/đêm). Bộ dữ liệu nhỏ này phản ánh đầy đủ các đặc tính của dữ liệu thực tế: Nhiều quận khác nhau, đa dạng loại phòng, chứa dữ liệu khuyết (NaN) và có giá trị ngoại lai.
Trong pandas 3, cơ chế Copy-on-Write (CoW) đã trở thành hành vi mặc định của toàn bộ thư viện, bảo đảm các đối tượng dẫn xuất hành xử độc lập khi ghi mà không cần thiết lập tùy chọn thủ công.
python
import numpy as np
import pandas as pd
from IPython.display import display
# Địa chỉ lưu trữ snapshot thực tế phục vụ đối chiếu và mở rộng
SNAPSHOT_URL = (
"https://data.insideairbnb.com/chile/rm/santiago/"
"2026-06-29/visualisations/listings.csv"
)
# 12 dòng dữ liệu mẫu giả lập (đơn vị: CLP/đêm)
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()ds_quan giữ sáu dòng của Ñuñoa để dùng trong các bài tiếp theo. Trong Jupyter, display(bang) hiển thị DataFrame thành bảng có tên cột và chỉ mục. Các bảng kết quả dưới lời giải dùng cùng dữ liệu này.
NOTE
Đối chiếu thực tế: Khi phân tích trên snapshot thực tế Santiago (18.534 dòng, 19 cột), toàn bộ thành phố có 846 chỗ ở khuyết giá. Riêng quận Ñuñoa chiếm 1.813 chỗ ở (khoảng 9,8% thị trường), với trung vị giá toàn quận đạt 60.981 CLP. Các con số đối chiếu thực tế được đưa vào nhằm giúp bạn kiểm chứng tính mở rộng của thuật toán, tuyệt đối không dùng làm hằng số cứng trong mã nguồn giải thuật.
3. Nội dung chi tiết bộ bài tập thực hành
Bài tập 1: Trích xuất cột, lọc hàng và tính tỷ lệ trên bảng dữ liệu
Bài tập 1: Chọn cột, chọn hàng và tính tỷ lệ
Cho một DataFrame df chứa ít nhất ba cột: name, room_type và price (kiểu số thực, có thể chứa giá trị khuyết NaN). Hãy viết hàm select_basics(df) trả về một từ điển (dict) gồm đúng 5 khóa sau:
cot_gia: Đối tượng Series chứa dữ liệu của cộtprice.bang_con: Đối tượng DataFrame gồm đúng hai cộtnamevàpricetheo đúng thứ tự đó.ty_le_private: Một số thực (float) biểu diễn tỷ lệ các dòng córoom_type == "Private room"trên tổng số dòng của bảng.nguyen_can: Đối tượng DataFrame chứa tất cả các dòng córoom_type == "Entire home/apt"với đầy đủ các cột ban đầu, được trích xuất bằng cú pháp mặt nạdf[mask].gia_nguyen_can: Đối tượng Series chứa giá trị cộtpricecủa đúng các dòng nguyên căn nói trên, được trích xuất bằng cú pháp.loc[mask, "price"].
Yêu cầu kỹ thuật: Giữ nguyên chỉ mục dòng (index) và thứ tự gốc của DataFrame. Không được làm biến đổi dữ liệu trên df truyền vào.
Lời giải
Cách 1: Trích xuất trực tiếp bằng chỉ mục và mặt nạ boolean cơ bản
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def select_basics(df: pd.DataFrame) -> dict:
# 1. Trích xuất một cột trả về đối tượng Series
cot_gia = df["price"]
# 2. Truyền một danh sách tên cột để trích xuất DataFrame con
bang_con = df[["name", "price"]]
# 3. Tính tỷ lệ bằng cách lấy trung bình cộng trên mảng boolean
ty_le_private = float((df["room_type"] == "Private room").mean())
# 4. Lọc hàng bằng cú pháp ngoặc vuông với mặt nạ điều kiện
mask_nc = df["room_type"] == "Entire home/apt"
nguyen_can = df[mask_nc]
# 5. Truy xuất chính xác cột mục tiêu của các hàng thỏa mãn bằng .loc
gia_nguyen_can = df.loc[mask_nc, "price"]
return {
"cot_gia": cot_gia,
"bang_con": bang_con,
"ty_le_private": ty_le_private,
"nguyen_can": nguyen_can,
"gia_nguyen_can": gia_nguyen_can,
}
ket_qua = select_basics(DEMO)
display(ket_qua["bang_con"])
print("Tỷ lệ phòng riêng:", ket_qua["ty_le_private"])
# Kết thúc lời giảiGiải thích cách 1
Một tên cột trong df["price"] chọn một Series, còn danh sách tên trong df[["name", "price"]] chọn DataFrame hai cột. Dấu ngoặc ngoài là thao tác chọn, danh sách bên trong xác định thứ tự cột. Hai kết quả giữ chỉ mục dòng ban đầu.
df["room_type"] == "Private room" tạo Series Boolean. Lấy mean() của nó cho tỷ lệ vì mỗi True đóng góp 1, mỗi False đóng góp 0. Mẫu có bốn phòng riêng trên mười hai dòng, nên tỷ lệ bằng một phần ba. Mặt nạ mask_nc chọn nguyên căn. df[mask_nc] giữ đủ cột, còn .loc[mask_nc, "price"] chỉ lấy giá tại các dòng đó.
Kết quả cách 1
| index | name | price |
|---|---|---|
| 0 | Depto Plaza Ñuñoa | 45000.0 |
| 1 | Pieza cerca metro | 18000.0 |
| 2 | Loft Irarrázaval | 72000.0 |
| 3 | Depto sin price | NaN |
| 4 | Dormitorio simple | 2500.0 |
| 5 | Casa familiar | 60000.0 |
| 6 | Studio Providencia | 80000.0 |
| 7 | Pieza Providencia | 30000.0 |
| 8 | Depto Centro | 40000.0 |
| 9 | Hostal Centro | 12000.0 |
| 10 | Pieza Centro | NaN |
| 11 | Hotel boutique | 95000.0 |
Dòng in riêng là Tỷ lệ phòng riêng: 0.3333333333333333. cot_gia có shape (12,), bang_con có shape (12, 2), nguyen_can có shape (6, 6) và gia_nguyen_can có shape (6,).
Cách 2: Sử dụng các phương thức tường minh và chuẩn hóa chỉ mục
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def select_basics(df: pd.DataFrame) -> dict:
# Truy xuất tường minh trục hàng và cột bằng .loc
cot_gia = df.loc[:, "price"]
# Sử dụng filter để trích xuất danh sách cột một cách an toàn
bang_con = df.filter(items=["name", "price"])
# Sử dụng phương thức so sánh .eq() để tạo Series boolean
mask_private = df["room_type"].eq("Private room")
ty_le_private = float(mask_private.mean())
# Xác định các hàng nguyên căn và lọc theo lát cắt
mask_entire = df["room_type"].eq("Entire home/apt")
nguyen_can = df.loc[mask_entire]
gia_nguyen_can = df.loc[mask_entire, "price"]
return {
"cot_gia": cot_gia,
"bang_con": bang_con,
"ty_le_private": ty_le_private,
"nguyen_can": nguyen_can,
"gia_nguyen_can": gia_nguyen_can,
}
ket_qua = select_basics(DEMO)
display(ket_qua["bang_con"])
print("Tỷ lệ phòng riêng:", ket_qua["ty_le_private"])
# Kết thúc lời giảiGiải thích cách 2
.loc[:, "price"] chọn mọi hàng và một cột, nên trả Series. filter(items=["name", "price"]) chọn cột theo tên và thứ tự trong danh sách. Khác cú pháp ngoặc vuông với danh sách, filter bỏ qua tên cột không có. Bài này đã bảo đảm có đủ cột, nên hai cách trả cùng bảng.
.eq("Private room") là phương thức so sánh bằng, tương đương biểu thức == ở đây. Mặt nạ nguyên căn được dùng lại cho cả bảng đầy đủ và Series giá. Kết quả trả về trong từ điển giữ riêng các đối tượng có vai trò khác nhau. Hàm chỉ đọc, không gán vào df nên không thay đổi các giá trị của bảng truyền vào.
Kết quả cách 2
| index | name | price |
|---|---|---|
| 0 | Depto Plaza Ñuñoa | 45000.0 |
| 1 | Pieza cerca metro | 18000.0 |
| 2 | Loft Irarrázaval | 72000.0 |
| 3 | Depto sin price | NaN |
| 4 | Dormitorio simple | 2500.0 |
| 5 | Casa familiar | 60000.0 |
| 6 | Studio Providencia | 80000.0 |
| 7 | Pieza Providencia | 30000.0 |
| 8 | Depto Centro | 40000.0 |
| 9 | Hostal Centro | 12000.0 |
| 10 | Pieza Centro | NaN |
| 11 | Hotel boutique | 95000.0 |
ty_le_private bằng 0.3333333333333333. Các chỉ mục của sáu dòng nguyên căn là 0, 2, 3, 5, 6, 8.
So sánh hai cách
Hai cách cùng chọn cột, lọc dòng và tính tỷ lệ. Cách 1 phù hợp khi học những cú pháp cơ bản. Cách 2 dùng phương thức có tên và chỉ rõ trục chọn bằng loc. Sự ngắn dài của câu lệnh không tạo một bằng chứng về tốc độ.
Bài tập 2: Thói quen 5 bước khám phá dữ liệu và rút trích chỉ số thống kê
Bài tập 2: Khám phá tổng quan bảng dữ liệu
Khi tiếp cận một tập dữ liệu bảng mới, một chuyên viên xử lý dữ liệu cần thực hiện thói quen 5 bước tiêu chuẩn:
- Kiểm tra kích thước bảng qua thuộc tính
.shape. - Lấy mẫu ngẫu nhiên các dòng bằng
.sample(). - Kiểm tra kiểu dữ liệu và số lượng giá trị hợp lệ bằng
.info(). - Xem bảng tóm tắt thống kê mô tả phân phối giá bằng
.describe(). - Đếm phân bố tần suất của các loại hình chỗ ở bằng
.value_counts().
Hãy viết hàm extract_5step_metrics(df) nhận vào DataFrame và tự động rút trích ra 4 chỉ số dưới dạng từ điển:
so_dong: Tổng số dòng của bảng dữ liệu.so_cot: Tổng số cột của bảng dữ liệu.so_thieu_price: Số lượng ô bị khuyết giá trị trong cộtprice.so_loai_phong: Số lượng các loại hình phòng khác nhau xuất hiện trong cộtroom_type.
Lời giải
Cách 1: Sử dụng các thuộc tính và hàm tính toán cơ bản
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def extract_5step_metrics(df: pd.DataFrame) -> dict:
# Bước 1: Rút trích số dòng và số cột từ shape
so_dong = len(df)
so_cot = df.shape[1]
# Bước 2 & 3: Đếm số giá trị khuyết bằng isna()
so_thieu_price = int(df["price"].isna().sum())
# Bước 4 & 5: Đếm số lượng loại phòng phân biệt qua value_counts()
so_loai_phong = len(df["room_type"].value_counts())
return {
"so_dong": so_dong,
"so_cot": so_cot,
"so_thieu_price": so_thieu_price,
"so_loai_phong": so_loai_phong,
}
display(pd.DataFrame([extract_5step_metrics(DEMO)]))
# Kết thúc lời giảiGiải thích cách 1
len(df) đếm số dòng và df.shape[1] đọc số cột. df["price"].isna() tạo mặt nạ giá thiếu, còn .sum() cộng số vị trí True. Có hai giá thiếu, ở chỉ mục 3 và 10, nên so_thieu_price bằng 2.
value_counts() tạo một số đếm cho mỗi loại phòng xuất hiện. Lấy len của Series đếm cho số loại, bằng 4 trên cột chuỗi trong mẫu. Các kiểu int trong từ điển giúp người dùng sau này nhận số đếm thông thường. Bảng này tóm tắt bốn chỉ số, không thay thế việc xem mẫu bằng sample, kiểu bằng info và phân phối bằng describe.
Kết quả cách 1
| so_dong | so_cot | so_thieu_price | so_loai_phong |
|---|---|---|---|
| 12 | 6 | 2 | 4 |
Cách 2: Đọc trực tiếp kích thước và đếm giá trị phân biệt bằng nunique
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def extract_5step_metrics(df: pd.DataFrame) -> dict:
# Rút trích trực tiếp bộ kích thước bảng
n_rows, n_cols = df.shape
# Đếm số lượng ô rỗng bằng isnull()
missing_price = int(df["price"].isnull().sum())
# Sử dụng nunique() để đếm số giá trị phân biệt
n_room_types = int(df["room_type"].nunique())
return {
"so_dong": n_rows,
"so_cot": n_cols,
"so_thieu_price": missing_price,
"so_loai_phong": n_room_types,
}
display(pd.DataFrame([extract_5step_metrics(DEMO)]))
# Kết thúc lời giảiGiải thích cách 2
n_rows, n_cols = df.shape tách tuple kích thước thành hai số. isnull() là tên khác của isna(), nên cho cùng mặt nạ thiếu. Hàm .nunique() đếm số giá trị phân biệt, mặc định bỏ qua giá trị thiếu, trả số loại trực tiếp mà không cần lập bảng tần suất đầy đủ.
Chọn nunique khi chỉ cần số loại giúp câu lệnh thể hiện đúng mục tiêu. Chọn value_counts khi còn cần biết số lượng từng loại. Với cột phân loại categorical có các mức chưa xuất hiện, cần để ý miền danh mục khi so sánh với độ dài một bảng đếm. Dữ liệu mẫu dùng các chuỗi thực sự xuất hiện nên hai phép đếm loại khớp nhau.
Kết quả cách 2
| so_dong | so_cot | so_thieu_price | so_loai_phong |
|---|---|---|---|
| 12 | 6 | 2 | 4 |
So sánh hai cách
Cách 1 tận dụng bảng tần suất để đếm loại. Cách 2 hỏi trực tiếp số giá trị phân biệt qua nunique. Cả hai trả cùng bốn chỉ số trên DEMO. Chưa có bước nào trong hai hàm tự kiểm tra chất lượng của mọi cột hoặc tính đủ mọi thống kê thăm dò.
Bài tập 3: Lọc hồ sơ theo quận và tính tỷ trọng thị trường
Bài tập 3: Cắt riêng bảng dữ liệu quận mục tiêu
Viết hàm filter_district(df: pd.DataFrame, ten_quan: str) -> dict để trích xuất toàn bộ dữ liệu của một quận cụ thể và đo lường quy mô của quận đó trong toàn thành phố.
Hàm nhận vào DataFrame df và chuỗi tên quận ten_quan (ví dụ: "Ñuñoa"). Kết quả trả về là một từ điển gồm đúng hai khóa:
bang: Đối tượng DataFrame gồm toàn bộ các dòng thuộc về quận được chỉ định, giữ nguyên đầy đủ mọi cột ban đầu cũng như chỉ mục gốc.ty_trong: Số thực biểu diễn tỷ lệ số chỗ ở của quận đó so với tổng số chỗ ở của toàn bảngdf.
Trường hợp biên: Nếu tên quận không tồn tại trong bảng dữ liệu, khóa bang phải trả về một DataFrame rỗng nhưng vẫn giữ nguyên vẹn cấu trúc các cột của bảng gốc, và ty_trong có giá trị là 0.0. Không được làm thay đổi DataFrame đầu vào.
Lời giải
Cách 1: Sử dụng phép so sánh mảng chuỗi truyền thống
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def filter_district(df: pd.DataFrame, ten_quan: str) -> dict:
# Tạo mặt nạ lọc theo tên quận
mask = df["neighbourhood"] == ten_quan
bang = df[mask]
# Tính tỷ trọng thị trường an toàn khi bảng có dữ liệu
tong_so = len(df)
ty_trong = float(len(bang) / tong_so) if tong_so > 0 else 0.0
return {"bang": bang, "ty_trong": ty_trong}
ket_qua = filter_district(DEMO, "Ñuñoa")
display(ket_qua["bang"][["id", "room_type", "price"]])
print("Tỷ trọng chỗ ở:", ket_qua["ty_trong"])
# Kết thúc lời giảiGiải thích cách 1
Mặt nạ df["neighbourhood"] == ten_quan có một giá trị Boolean cho mỗi dòng. df[mask] giữ các dòng True với toàn bộ cột và chỉ mục gốc. Với tên Ñuñoa, sáu dòng đầu của DEMO được chọn.
Tỷ trọng lấy len(bang) chia len(df), tức 6 / 12 = 0.5 trên tập mẫu. Điều kiện tong_so > 0 tránh chia cho không nếu bảng đầu vào rỗng. Nếu tên quận không tồn tại, phép lọc trả bảng không dòng nhưng vẫn giữ cấu trúc các cột, và số dòng được chọn bằng 0 nên tỷ trọng cũng bằng 0.0.
Kết quả cách 1
| index | id | room_type | price |
|---|---|---|---|
| 0 | 101 | Entire home/apt | 45000.0 |
| 1 | 102 | Private room | 18000.0 |
| 2 | 103 | Entire home/apt | 72000.0 |
| 3 | 104 | Entire home/apt | NaN |
| 4 | 105 | Private room | 2500.0 |
| 5 | 106 | Entire home/apt | 60000.0 |
Dòng in riêng là Tỷ trọng chỗ ở: 0.5. Hàm giữ cả sáu cột trong bang, bảng trên chỉ hiển thị ba cột để đối chiếu.
Cách 2: Sử dụng phương thức .eq() và tính trung bình trực tiếp trên mặt nạ boolean
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def filter_district(df: pd.DataFrame, ten_quan: str) -> dict:
# Sử dụng .eq() để tạo Series boolean
mask = df["neighbourhood"].eq(ten_quan)
bang = df.loc[mask]
# Lấy trung bình cộng của mặt nạ boolean để có ngay tỷ trọng
ty_trong = float(mask.mean()) if len(df) > 0 else 0.0
return {"bang": bang, "ty_trong": ty_trong}
ket_qua = filter_district(DEMO, "Ñuñoa")
display(ket_qua["bang"][["id", "room_type", "price"]])
print("Tỷ trọng chỗ ở:", ket_qua["ty_trong"])
# Kết thúc lời giảiGiải thích cách 2
.eq(ten_quan) tạo cùng mặt nạ so sánh tên, và .loc[mask] ghi rõ rằng mặt nạ dùng cho trục hàng. mask.mean() tính tỷ lệ số dòng True trong toàn bảng, tương đương số dòng quận chia tổng số dòng vì cột tên quận của mẫu không khuyết.
Nhánh len(df) > 0 đặt tỷ trọng 0.0 khi không có dòng. Hàm không tự chuẩn hóa tên, nên khác dấu, khoảng trắng hoặc cách viết vẫn có thể không khớp. Ñuñoa cần được truyền đúng như dữ liệu. Kết quả không đặt lại chỉ mục, giúp truy về dòng gốc khi rà soát.
Kết quả cách 2
| index | id | room_type | price |
|---|---|---|---|
| 0 | 101 | Entire home/apt | 45000.0 |
| 1 | 102 | Private room | 18000.0 |
| 2 | 103 | Entire home/apt | 72000.0 |
| 3 | 104 | Entire home/apt | NaN |
| 4 | 105 | Private room | 2500.0 |
| 5 | 106 | Entire home/apt | 60000.0 |
ty_trong bằng 0.5, tức 50% của DEMO, không phải 50% thị trường Santiago.
So sánh hai cách
Cách 1 tính tử số và mẫu số bằng số dòng. Cách 2 lấy trung bình của chính mặt nạ chọn. Hai cách tương đương trên cột tên quận không thiếu của mẫu. Khi dữ liệu tên quận có thiếu, cần thống nhất cách tính mẫu số trước khi dùng mẹo Boolean.
Bài tập 4: Lọc kết hợp đa điều kiện logic bằng toán tử bitwise
Bài tập 4: Đếm số chỗ ở phân khúc cạnh tranh mục tiêu
Nhà đầu tư đặt câu hỏi cụ thể: "Tại quận Ñuñoa, hiện có bao nhiêu căn nguyên căn với mức giá thuê từ 60.000 CLP trở xuống mỗi đêm?"
Hãy viết hàm count_segment(listings: pd.DataFrame, room_type: str = "Entire home/apt", gia_toi_da: float = 60000.0) -> int. Hàm nhận vào DataFrame của một quận (chứa các cột room_type và price) cùng hai tham số ngưỡng lọc. Kết quả trả về là một số nguyên (int): Số lượng dòng thỏa mãn đồng thời cả hai điều kiện:
- Cột
room_typecó giá trị đúng bằng tham số truyền vào. - Cột
pricecó giá trị nhỏ hơn hoặc bằnggia_toi_da.
Ràng buộc kỹ thuật: Các chỗ ở bị khuyết giá (NaN) tuyệt đối không được tính vào kết quả. Nếu bảng đầu vào rỗng, hàm trả về 0. Bắt buộc sử dụng mặt nạ boolean vector hóa, không dùng vòng lặp for.
Lời giải
Cách 1: Sử dụng toán tử bitwise & kết hợp cặp ngoặc đơn bắt buộc
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def count_segment(
listings: pd.DataFrame,
room_type: str = "Entire home/apt",
gia_toi_da: float = 60000.0,
) -> int:
if len(listings) == 0:
return 0
# Bắt buộc bao quanh mỗi biểu thức so sánh bằng dấu ngoặc đơn
mask = (listings["room_type"] == room_type) & (listings["price"] <= gia_toi_da)
return int(mask.sum())
display(pd.DataFrame([{"so_cho_o": count_segment(ds_quan)}]))
# Kết thúc lời giảiGiải thích cách 1
Mỗi điều kiện so sánh tạo một Series Boolean. Dấu & kết hợp theo từng vị trí và chỉ cho True khi loại phòng đúng đồng thời giá không vượt ngưỡng. Ngoặc quanh từng điều kiện bảo đảm Python nhóm đúng biểu thức trước khi kết hợp.
Trên cột float64 của mẫu, NaN <= 60000 là False nên dòng thiếu giá không được tính. .sum() của mặt nạ là số dòng thỏa mãn. Hai mã 101 và 106 là nguyên căn có giá lần lượt 45000, 60000. Giá đúng bằng ngưỡng được giữ vì đề yêu cầu “từ ngưỡng trở xuống”, tức dùng <=.
Kết quả cách 1
| so_cho_o |
|---|
| 2 |
Cách 2: Tách điều kiện thành các biến trung gian bằng .eq() và .le()
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def count_segment(
listings: pd.DataFrame,
room_type: str = "Entire home/apt",
gia_toi_da: float = 60000.0,
) -> int:
if len(listings) == 0:
return 0
# Sử dụng .eq() và .le() để biểu đạt mạch lạc
mask_type = listings["room_type"].eq(room_type)
mask_price = listings["price"].le(gia_toi_da)
return int((mask_type & mask_price).sum())
display(pd.DataFrame([{"so_cho_o": count_segment(ds_quan)}]))
# Kết thúc lời giảiGiải thích cách 2
.eq(room_type) tạo mặt nạ đúng loại, còn .le(gia_toi_da) tạo mặt nạ giá nhỏ hơn hoặc bằng ngưỡng. Đặt chúng vào hai biến giúp đọc hoặc kiểm tra từng điều kiện trước khi dùng mask_type & mask_price để kết hợp.
int(...) chuyển kết quả đếm sang số nguyên Python. Cả hai mặt nạ có cùng chỉ mục vì được tạo từ cùng một DataFrame. Không dùng and cho hai Series, vì and yêu cầu một giá trị đúng/sai cho toàn đối tượng trong khi ở đây cần quyết định riêng cho từng dòng.
Kết quả cách 2
| so_cho_o |
|---|
| 2 |
So sánh hai cách
Hai cách thực hiện cùng phép lọc vector hóa. Cách 1 viết biểu thức trực tiếp, Cách 2 đặt tên từng điều kiện bằng phương thức so sánh. Hai dòng đạt điều kiện có thể được đối chiếu bằng chính mã phòng, không chỉ bằng tổng số 2.
Bài tập 5: Truy xuất top chỗ ở giá thấp nhất bằng nsmallest và gắn cờ bất thường
Bài tập 5: Tìm n chỗ ở có mức giá thấp nhất quận
Viết hàm cheapest(listings: pd.DataFrame, n: int = 5) -> pd.DataFrame nhận vào bảng dữ liệu của một quận (chứa các cột name và price, có thể chứa NaN). Tham số n là số nguyên dương và không vượt quá số dòng có giá hợp lệ.
Hàm trả về một DataFrame gồm đúng name và price theo đúng thứ tự đó. Kết quả được sắp xếp theo mức giá tăng dần và phải bảo toàn chỉ mục dòng ban đầu. Trong trường hợp các dòng có mức giá bằng nhau, ưu tiên giữ thứ tự dòng xuất hiện trước trong bảng.
Đồng thời, hãy đưa ra nhận xét chuyên môn về giá trị nhỏ nhất tìm được trên dữ liệu thực tế.
Lời giải
Cách 1: Sử dụng phương thức nsmallest
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def cheapest(listings: pd.DataFrame, n: int = 5) -> pd.DataFrame:
# Trong miền n đã cho, kết quả đủ n giá hợp lệ và giữ index gốc
top_cheap = listings.nsmallest(n, "price")
return top_cheap[["name", "price"]]
display(cheapest(ds_quan, n=5))
# Kết thúc lời giảiGiải thích cách 1
nsmallest(n, "price") chọn tối đa n dòng có giá nhỏ nhất và xếp giá tăng dần. Trong miền n của đề, n không vượt số giá hợp lệ của quận. Mặc định keep="first" ưu tiên dòng xuất hiện trước khi các giá bằng nhau.
Kết quả còn toàn bộ cột, nên dòng trả về chọn lại name và price theo đúng thứ tự yêu cầu. Chỉ mục gốc được giữ, thể hiện qua thứ tự 4, 1, 0, 5, 2. Giá nhỏ nhất trong DEMO là 2500.0 CLP. Đây là giá giả lập để kiểm thử, không phải kết luận về mức giá thực tế. Với dữ liệu thật, cần kiểm tra đơn vị, loại phòng và bản ghi trước khi diễn giải một giá quá thấp.
Kết quả cách 1
| index | name | price |
|---|---|---|
| 4 | Dormitorio simple | 2500.0 |
| 1 | Pieza cerca metro | 18000.0 |
| 0 | Depto Plaza Ñuñoa | 45000.0 |
| 5 | Casa familiar | 60000.0 |
| 2 | Loft Irarrázaval | 72000.0 |
Cách 2: Lọc bỏ giá trị thiếu và sắp xếp với thuật toán ổn định
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def cheapest(listings: pd.DataFrame, n: int = 5) -> pd.DataFrame:
# Loại bỏ giá trị khuyết, sắp xếp ổn định và lấy n dòng đầu
sorted_df = (
listings.dropna(subset=["price"])
.sort_values(by="price", ascending=True, kind="stable")
.head(n)
)
return sorted_df.loc[:, ["name", "price"]]
display(cheapest(ds_quan, n=5))
# Kết thúc lời giảiGiải thích cách 2
dropna(subset=["price"]) chỉ bỏ dòng thiếu ở cột giá, không loại vì các cột khác có thiếu. sort_values(..., kind="stable") xếp giá tăng dần và giữ thứ tự tương đối của các dòng hòa giá. head(n) sau đó lấy n dòng đầu, còn .loc[:, ["name", "price"]] khóa hai cột đầu ra.
Cách này diễn đạt rõ từng bước loại giá thiếu, sắp xếp và cắt đầu danh sách. Nó sắp toàn bộ các dòng hợp lệ dù chỉ cần một số dòng đầu. Giữ chỉ mục gốc giúp đối chiếu trực tiếp với bảng ban đầu và phát hiện xem dòng thiếu giá có vô tình lọt vào kết quả hay không.
Kết quả cách 2
| index | name | price |
|---|---|---|
| 4 | Dormitorio simple | 2500.0 |
| 1 | Pieza cerca metro | 18000.0 |
| 0 | Depto Plaza Ñuñoa | 45000.0 |
| 5 | Casa familiar | 60000.0 |
| 2 | Loft Irarrázaval | 72000.0 |
So sánh hai cách
Cách 1 dùng thao tác chọn cực trị nhỏ nhất, thường thích hợp khi chỉ cần một ít dòng đầu từ bảng lớn. Cách 2 sắp toàn bộ để quan sát thứ tự rõ ràng. Chúng cho cùng năm dòng trên mẫu và cùng quy tắc giữ thứ tự hòa giá, nhưng cần đo trên dữ liệu thực nếu muốn kết luận chênh lệch thời gian.
Bài tập 6: Cập nhật giá trị bằng .loc và đồng bộ hóa cột phụ thuộc
Bài tập 6: Mô phỏng chính sách giảm giá và tính lại báo giá 3 đêm
Nhà đầu tư muốn mô phỏng một chiến lược giá mới: Áp dụng mức giảm giá Entire home/apt), đồng thời tính toán tổng số tiền thuê cho kỳ nghỉ 3 đêm.
Hãy viết hàm discount_quote(listings: pd.DataFrame, room_type: str = "Entire home/apt", giam: float = 0.10) -> pd.DataFrame. Hàm nhận vào DataFrame quận (có các cột id, room_type, price) và tỷ lệ giảm giá giam (id, room_type, price, tien_3_dem.
- Cột
pricecủa các dòng thỏa mãn loại phòng được giảm theo biểu thứcbằng cú pháp .loc[mask, "price"]. Các dòng khác giữ nguyên giá. - Cột
tien_3_demđược tính bằng, căn cứ trên mức giá sau khi đã giảm. Các ô có giá NaNvẫn giữ nguyên làNaN.
Ràng buộc: Tuyệt đối không làm thay đổi DataFrame listings gốc (thao tác trên một bản sao .copy()). Không sử dụng cú pháp hai lần ngoặc vuông.
Lời giải
Cách 1: Thao tác tuần tự bằng .loc và gán cột mới
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def discount_quote(
listings: pd.DataFrame, room_type: str = "Entire home/apt", giam: float = 0.10
) -> pd.DataFrame:
# 1. Tạo bản sao độc lập để không ảnh hưởng dữ liệu gốc
df_quote = listings[["id", "room_type", "price"]].copy()
# 2. Tạo mặt nạ lọc các căn hộ mục tiêu
mask = df_quote["room_type"] == room_type
# 3. Cập nhật giá trực tiếp vào bảng thông qua .loc
df_quote.loc[mask, "price"] = df_quote.loc[mask, "price"] * (1.0 - giam)
# 4. Tính toán cột phụ thuộc từ mức giá mới đã cập nhật
df_quote["tien_3_dem"] = df_quote["price"] * 3
return df_quote
display(discount_quote(ds_quan))
# Kết thúc lời giảiGiải thích cách 1
Hàm chọn ba cột đầu vào rồi .copy() để tạo bảng làm việc riêng. Mặt nạ chọn dòng nguyên căn. .loc[mask, "price"] xuất hiện ở cả vế trái và vế phải của phép gán, nên dữ liệu được đọc, nhân hệ số giảm rồi ghi vào đúng cột của bảng này trong một thao tác chọn hàng/cột rõ ràng.
Với giam=0.10, hệ số còn lại là 0.9. Sau khi cập nhật, hàm mới tính tien_3_dem = price * 3, nên mã 101 có giá mới 40500 và tiền ba đêm 121500. Cột tiền ba đêm là giá trị đã tính, không phải công thức tự cập nhật như ô bảng tính. Nếu đổi giá sau đó, phải tính lại cột phụ thuộc.
Kết quả cách 1
| id | room_type | price | tien_3_dem |
|---|---|---|---|
| 101 | Entire home/apt | 40500.0 | 121500.0 |
| 102 | Private room | 18000.0 | 54000.0 |
| 103 | Entire home/apt | 64800.0 | 194400.0 |
| 104 | Entire home/apt | NaN | NaN |
| 105 | Private room | 2500.0 | 7500.0 |
| 106 | Entire home/apt | 54000.0 | 162000.0 |
Cách 2: Sử dụng toán tử kết hợp và phương thức .assign()
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def discount_quote(
listings: pd.DataFrame, room_type: str = "Entire home/apt", giam: float = 0.10
) -> pd.DataFrame:
df_quote = listings[["id", "room_type", "price"]].copy()
mask = df_quote["room_type"].eq(room_type)
# Sử dụng toán tử nhân gán kết hợp trên lát cắt .loc
df_quote.loc[mask, "price"] *= 1.0 - giam
# Gán cột phụ thuộc bằng biểu thức lambda thông qua .assign()
return df_quote.assign(tien_3_dem=lambda x: x["price"] * 3)
display(discount_quote(ds_quan))
# Kết thúc lời giảiGiải thích cách 2
.eq(room_type) tạo mặt nạ như phép so sánh bằng. Phép nhân gán trên .loc[mask, "price"] vẫn chỉ rõ một lần chọn hàng và cột của bảng đích. Các dòng không thỏa mặt nạ giữ nguyên giá, còn giá NaN vẫn là NaN sau phép nhân.
assign(tien_3_dem=lambda x: x["price"] * 3) tạo cột trong một DataFrame trả về. Lambda nhận bảng x đang có giá đã giảm, vì bước cập nhật thực hiện trước lời gọi assign. Đặt phép tính phụ thuộc ở cuối bảo đảm không vô tình dùng giá cũ để báo tổng tiền. Tỷ lệ giam trong ví dụ nằm trong miền từ 0 đến dưới 1 như đề yêu cầu.
Kết quả cách 2
| id | room_type | price | tien_3_dem |
|---|---|---|---|
| 101 | Entire home/apt | 40500.0 | 121500.0 |
| 102 | Private room | 18000.0 | 54000.0 |
| 103 | Entire home/apt | 64800.0 | 194400.0 |
| 104 | Entire home/apt | NaN | NaN |
| 105 | Private room | 2500.0 | 7500.0 |
| 106 | Entire home/apt | 54000.0 | 162000.0 |
So sánh hai cách
Hai cách cùng tạo bảng riêng, giảm đúng dòng rồi tính tiền dựa trên giá mới. Cách 1 dùng phép gán cột trực tiếp, Cách 2 dùng assign để tạo kết quả trả về. Chúng giữ giá thiếu thay vì thay bằng không, nên không tạo báo giá giả cho chỗ chưa biết giá.
Bài tập 7: So sánh ngưỡng thống kê và phân tích độ lệch mẫu số do dữ liệu khuyết (NaN)
Bài tập 7: Phân tích tác động của NaN lên tỷ lệ giá dưới ngưỡng
Một thước đo phổ biến để đánh giá tính cạnh tranh của một quận là so sánh giá thuê từng chỗ ở trong quận với mức giá trung vị của toàn thành phố (med_tp = df["price"].median()). Ta muốn xác định tỷ lệ chỗ ở của quận có giá thấp hơn mức trung vị thành phố.
Hãy viết hàm share_below(prices: pd.Series, nguong: float) -> dict nhận vào một Series giá (có thể chứa NaN, có ít nhất một giá hợp lệ) và ngưỡng so sánh nguong. Hàm trả về một từ điển gồm đúng 3 khóa:
ty_le_tho: Tỷ lệ tính trực tiếp theo cách thô bằng cách lấy trung bình trên toàn bộ các dòng(prices < nguong).mean().ty_le_sach: Tỷ lệ tính sạch, trong đó mẫu số chỉ bao gồm các dòng thực sự có giá trị giá hợp lệ (loại bỏNaN).so_thieu: Số lượng giá trịNaNxuất hiện trong chuỗiprices.
Giải thích rõ nguyên nhân dẫn đến sự chênh lệch giữa hai tỷ lệ này.
Lời giải
Cách 1: Sử dụng mean trực tiếp và phương thức dropna()
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def share_below(prices: pd.Series, nguong: float) -> dict:
# 1. Tính toán thô: Mẫu số bao gồm cả các dòng khuyết giá
ty_le_tho = float((prices < nguong).mean())
# 2. Chỉ xét hàng có giá: Lọc bỏ NaN trước khi tính tỷ lệ
gia_sach = prices.dropna()
ty_le_sach = float((gia_sach < nguong).mean()) if len(gia_sach) > 0 else 0.0
# 3. Đếm số lượng giá trị thiếu
so_thieu = int(prices.isna().sum())
return {
"ty_le_tho": ty_le_tho,
"ty_le_sach": ty_le_sach,
"so_thieu": so_thieu,
}
display(pd.DataFrame([share_below(ds_quan["price"], DEMO["price"].median())]))
# Kết thúc lời giảiGiải thích cách 1
DEMO["price"].median() bỏ hai NaN và cho ngưỡng thành phố bằng 42500.0. Trong Ñuñoa có hai giá dưới ngưỡng, là 18000 và 2500. Với cột float64 của fixture, so sánh NaN với ngưỡng cho False, nên (prices < nguong).mean() lấy tử số 2 nhưng mẫu số toàn sáu dòng.
dropna() tạo Series chỉ còn năm giá đọc được. So sánh trên Series này cho cùng tử số 2, nhưng mẫu số đúng với câu hỏi “trong các chỗ có giá” là 5, nên tỷ lệ sạch bằng 0.4. isna().sum() ghi riêng một giá thiếu để người đọc biết phạm vi của tỷ lệ.
Kết quả cách 1
| ty_le_tho | ty_le_sach | so_thieu |
|---|---|---|
| 0.333333 | 0.4 | 1 |
ty_le_tho là 2/6, còn ty_le_sach là 2/5. Bảng hiển thị đã rút gọn số chữ số của tỷ lệ thứ nhất.
Cách 2: Phân tích mẫu số tường minh bằng .lt() và .notna()
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def share_below(prices: pd.Series, nguong: float) -> dict:
so_thieu = int(prices.isnull().sum())
# Tạo mặt nạ boolean các phần tử nhỏ hơn ngưỡng
mask_below = prices.lt(nguong)
ty_le_tho = float(mask_below.mean())
# Đếm số phần tử hợp lệ làm mẫu số chuẩn mực
so_hop_le = int(prices.notna().sum())
ty_le_sach = float(mask_below.sum() / so_hop_le) if so_hop_le > 0 else 0.0
return {
"ty_le_tho": ty_le_tho,
"ty_le_sach": ty_le_sach,
"so_thieu": so_thieu,
}
display(pd.DataFrame([share_below(ds_quan["price"], DEMO["price"].median())]))
# Kết thúc lời giảiGiải thích cách 2
.lt(nguong) tương đương phép so sánh nhỏ hơn. mask_below.sum() đếm số dòng thực sự dưới ngưỡng. prices.notna().sum() đếm năm giá hợp lệ làm mẫu số, nên hàm không cần tạo Series giá đã bỏ thiếu chỉ để tính tỷ lệ này.
isnull() là tên khác của isna và cho số giá thiếu như Cách 1. Kết luận về tỷ lệ thô đang dựa trên cột float64 chứa np.nan trong DEMO. Với kiểu nullable Float64 và pd.NA, phép so sánh có thể tạo Boolean thiếu rồi mean bỏ qua chúng. Vì thế cần kiểm tra dtype và quy tắc thiếu thay vì khẳng định mọi phép so sánh pandas đều đưa NaN vào mẫu số giống nhau.
Kết quả cách 2
| ty_le_tho | ty_le_sach | so_thieu |
|---|---|---|
| 0.333333 | 0.4 | 1 |
So sánh hai cách
Cách 1 tạo tập giá hợp lệ rồi tính tỷ lệ. Cách 2 đếm riêng tử số và mẫu số để tránh tạo Series lọc. Hai cách trả 0.4 trên mẫu, đồng thời ghi số thiếu để không che mất một dòng chưa biết giá.
Bài tập 8: Phân tích cơ cấu loại phòng và tỷ lệ khuyết dữ liệu
Bài tập 8: Tổng hợp phân bố loại hình và tỷ lệ dữ liệu thiếu
Viết hàm room_mix(listings: pd.DataFrame) -> dict nhận vào bảng dữ liệu của một quận (chứa cột room_type kiểu chuỗi không khuyết và cột price kiểu số thực có thể chứa NaN).
Hàm trả về một từ điển gồm đúng hai thành phần:
co_cau: Một đối tượng Series biểu diễn tỷ lệ phân bổ của các loại hình phòng, được tạo bởi phương thứcvalue_counts(normalize=True). Chỉ mục của Series là tên các loại phòng, giá trị là tỷ lệ trong khoảng. thieu_pct: Một số thực biểu diễn phần trăm số dòng bị thiếu giá trong quận (thang đođến ), được làm tròn đến đúng 2 chữ số thập phân.
Lời giải
Cách 1: Sử dụng value_counts(normalize=True) và phép toán boolean
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def room_mix(listings: pd.DataFrame) -> dict:
# 1. Chuẩn hóa tần suất loại phòng về tỷ lệ tổng bằng 1.0
co_cau = listings["room_type"].value_counts(normalize=True)
# 2. Tính phần trăm số ô bị khuyết giá và làm tròn 2 chữ số
thieu_pct = round(float(listings["price"].isna().mean() * 100), 2)
return {"co_cau": co_cau, "thieu_pct": thieu_pct}
ket_qua = room_mix(ds_quan)
display(ket_qua["co_cau"].rename("ty_le").to_frame())
print("Tỷ lệ thiếu giá (%):", ket_qua["thieu_pct"])
# Kết thúc lời giảiGiải thích cách 1
value_counts(normalize=True) đếm từng loại rồi chia cho tổng số loại phòng được quan sát. Do cột room_type của đề không thiếu, mẫu số là đủ sáu dòng quận. Bốn nguyên căn cho tỷ lệ 4/6, hai phòng riêng cho tỷ lệ 2/6. Các giá trị Series nằm trên thang 0–1.
price.isna() đánh dấu một giá thiếu. Lấy mean cho tỷ lệ 1/6, nhân 100 chuyển sang phần trăm rồi round(..., 2) thành 16.67. Khi trình bày, rename("ty_le").to_frame() đặt tên và chuyển Series thành DataFrame một cột để notebook hiển thị bảng rõ ràng.
Kết quả cách 1
| room_type | ty_le |
|---|---|
| Entire home/apt | 0.666667 |
| Private room | 0.333333 |
Dòng in riêng là Tỷ lệ thiếu giá (%): 16.67.
Cách 2: Sử dụng groupby kết hợp xử lý trường hợp bảng rỗng
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
import pandas as pd
def room_mix(listings: pd.DataFrame) -> dict:
tong_so = len(listings)
if tong_so == 0:
return {"co_cau": pd.Series(dtype=float), "thieu_pct": 0.0}
# Tính cơ cấu bằng groupby và sắp xếp giảm dần
co_cau = (
listings.groupby("room_type", observed=False).size() / tong_so
).sort_values(ascending=False)
thieu_pct = round(float(listings["price"].isnull().sum() / tong_so * 100), 2)
return {"co_cau": co_cau, "thieu_pct": thieu_pct}
ket_qua = room_mix(ds_quan)
display(ket_qua["co_cau"].rename("ty_le").to_frame())
print("Tỷ lệ thiếu giá (%):", ket_qua["thieu_pct"])
# Kết thúc lời giảiGiải thích cách 2
groupby("room_type", observed=False).size() đếm số dòng từng loại và giữ tên loại ở chỉ mục. Chia Series số đếm cho tong_so chuẩn hóa về tỷ lệ. sort_values(ascending=False) đặt nhóm lớn hơn trước. Với cột chuỗi của DEMO, hai nhóm và hai tỷ lệ khớp Cách 1.
Hàm tính tỷ lệ thiếu từ số ô thiếu chia tổng dòng, rồi làm tròn trên thang phần trăm. Nếu bảng rỗng, nhánh đầu trả Series rỗng và tỷ lệ 0.0 theo quy ước kỹ thuật. Với dữ liệu categorical, observed=False có thể giữ cả mức danh mục chưa xuất hiện, nên khi dùng lại phải phân biệt danh mục khai báo và loại thật sự có quan sát.
Kết quả cách 2
| room_type | ty_le |
|---|---|
| Entire home/apt | 0.666667 |
| Private room | 0.333333 |
Dòng in riêng là Tỷ lệ thiếu giá (%): 16.67.
So sánh hai cách
Cách 1 dùng công cụ chuyên đếm cơ cấu, Cách 2 khai triển cơ cấu thành số đếm theo nhóm chia quy mô. Chúng trả cùng kết quả trên bảng không rỗng. Quy ước 0.0 khi không có dòng ở Cách 2 cần được ghi rõ, vì không có quan sát không đồng nghĩa thị trường có tỷ lệ thiếu bằng không.
Bài tập 9: Đóng gói hồ sơ quận xuất file CSV và kiểm chứng tính bất biến
Bài tập 9: Xuất bản báo cáo hồ sơ quận ra tệp CSV và kiểm tra tính toàn vẹn
Hãy hoàn thiện bức tranh phân tích bằng cách viết hàm export_profile(listings: pd.DataFrame, ten_quan: str, path) -> pd.DataFrame. Hàm nhận vào DataFrame của quận, tên quận và đường dẫn tệp lưu trữ path. Hàm thực hiện các bước:
- Tổng hợp các chỉ số thành một DataFrame đúng một dòng với đúng 5 cột theo thứ tự sau:
quan: Chuỗi tên quận (ten_quan).so_cho_o: Số nguyên biểu diễn tổng số chỗ ở của quận.gia_trung_vi: Số thực biểu diễn trung vị giá thuê (bỏ quaNaN).ty_le_nguyen_can: Số thực biểu diễn tỷ lệ chỗ ở nguyên căn, làm tròn 3 chữ số thập phân.ty_le_thieu_gia_pct: Số thực biểu diễn phần trăm dòng thiếu giá, làm tròn 2 chữ số thập phân.
- Ghi DataFrame này ra đĩa cứng dưới định dạng CSV bằng
to_csv(path, index=False, encoding="utf-8"). - Đọc lại tệp CSV vừa ghi bằng
pd.read_csv(path)và trả về DataFrame đã đọc để kiểm tra các bất biến.
Lời giải
Cách 1: Khởi tạo DataFrame từ danh sách từ điển và xuất file
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
from pathlib import Path
import pandas as pd
def export_profile(listings: pd.DataFrame, ten_quan: str, path) -> pd.DataFrame:
# 1. Tính toán các chỉ số thành phần
so_cho_o = len(listings)
gia_trung_vi = float(listings["price"].median())
ty_le_nguyen_can = round(
float((listings["room_type"] == "Entire home/apt").mean()), 3
)
ty_le_thieu_gia_pct = round(float(listings["price"].isna().mean() * 100), 2)
# 2. Đóng gói vào một dòng dữ liệu duy nhất
ho_so = pd.DataFrame(
[
{
"quan": ten_quan,
"so_cho_o": so_cho_o,
"gia_trung_vi": gia_trung_vi,
"ty_le_nguyen_can": ty_le_nguyen_can,
"ty_le_thieu_gia_pct": ty_le_thieu_gia_pct,
}
]
)
# 3. Ghi ra tệp không lưu cột chỉ mục và đọc lại
ho_so.to_csv(path, index=False, encoding="utf-8")
doc_lai = pd.read_csv(path)
return doc_lai
display(export_profile(ds_quan, "Ñuñoa", "ho-so-nunoa.csv"))
# Kết thúc lời giảiGiải thích cách 1
Hàm tính năm trường trước khi tạo DataFrame một dòng từ danh sách chứa một từ điển. median() bỏ giá NaN, nên năm giá hợp lệ trong quận có trung vị 45000. Tỷ lệ nguyên căn là 4/6 được làm tròn thành 0.667, còn phần trăm thiếu giá là 16.67.
to_csv(..., index=False, encoding="utf-8") ghi tên cột và dữ liệu nhưng không thêm chỉ mục dòng thành cột CSV. UTF-8 giữ tên Ñuñoa đúng ký tự. read_csv(path) đọc lại thành bảng để đối chiếu tên cột, số dòng và các giá trị. Các tỷ lệ đã làm tròn dùng để báo cáo, không nên dùng thay dữ liệu gốc khi cần tính toán chính xác hơn.
Kết quả cách 1
| quan | so_cho_o | gia_trung_vi | ty_le_nguyen_can | ty_le_thieu_gia_pct |
|---|---|---|---|---|
| Ñuñoa | 6 | 45000.0 | 0.667 | 16.67 |
Cách 2: Khởi tạo có định kiểu dữ liệu tường minh khi đọc lại bằng dtype
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
ds_quan = DEMO.loc[DEMO["neighbourhood"].eq("Ñuñoa")].copy()
# Bắt đầu lời giải
from pathlib import Path
import pandas as pd
def export_profile(listings: pd.DataFrame, ten_quan: str, path) -> pd.DataFrame:
# Đóng gói dữ liệu dạng cột sang danh sách giá trị
du_lieu = {
"quan": [ten_quan],
"so_cho_o": [int(len(listings))],
"gia_trung_vi": [float(listings["price"].median())],
"ty_le_nguyen_can": [
round(float(listings["room_type"].eq("Entire home/apt").mean()), 3)
],
"ty_le_thieu_gia_pct": [
round(float(listings["price"].isnull().sum() / len(listings) * 100), 2)
],
}
ho_so = pd.DataFrame(du_lieu)
# Ghi tệp mã hóa UTF-8 chuẩn mực
ho_so.to_csv(path, index=False, encoding="utf-8")
# Đọc lại với schema kiểu dữ liệu định nghĩa trước
schema_types = {
"quan": str,
"so_cho_o": int,
"gia_trung_vi": float,
"ty_le_nguyen_can": float,
"ty_le_thieu_gia_pct": float,
}
return pd.read_csv(path, dtype=schema_types)
display(export_profile(ds_quan, "Ñuñoa", "ho-so-nunoa.csv"))
# Kết thúc lời giảiGiải thích cách 2
Từ điển du_lieu chứa một danh sách một phần tử cho mỗi cột, nên pd.DataFrame tạo đúng một dòng. Đây là cách dựng bảng theo cột, khác với từ điển biểu diễn một dòng ở Cách 1. Các phép tính dùng cùng dữ liệu và mức làm tròn.
schema_types ánh xạ tên cột sang kiểu mong muốn khi đọc lại CSV. Đối số dtype=schema_types giúp cột so_cho_o được đọc thành số nguyên và các thống kê thành số thực. Kiểu dữ liệu đọc không được suy ra chỉ từ hình dạng chữ trên màn hình. Bảng trả về sau đọc là đối tượng có thể dùng để kiểm tra các điều kiện như số cột, đơn vị và tính không âm của số đếm.
Kết quả cách 2
| quan | so_cho_o | gia_trung_vi | ty_le_nguyen_can | ty_le_thieu_gia_pct |
|---|---|---|---|---|
| Ñuñoa | 6 | 45000.0 | 0.667 | 16.67 |
So sánh hai cách
Hai cách ghi cùng nội dung CSV và đọc lại thành một dòng hồ sơ. Cách 1 để pandas suy kiểu, Cách 2 chỉ định kiểu khi đọc. CSV không lưu toàn bộ thông tin dtype như một định dạng có schema, nên bước đọc lại và đối chiếu vẫn cần thiết.
Bài tập 10 (mở rộng 1): Tự động hóa lập hồ sơ quận qua tham số cấu hình
Bài tập 10: Thiết kế pipeline lập hồ sơ tái sử dụng cho quận bất kỳ
Trong thực tế phát triển phần mềm và phân tích dữ liệu, ta không viết mã nguồn cố định (hardcode) tên một quận trong toàn bộ logic tính toán. Thay vào đó, toàn bộ quy trình từ Bài tập 2 đến Bài tập 8 cần được trừu tượng hóa thành một hàm pipeline có thể tái sử dụng cho bất kỳ quận nào chỉ bằng cách thay đổi một tham số cấu hình ten_quan.
Hãy thiết kế hàm profile_district_pipeline(df: pd.DataFrame, ten_quan: str) -> dict nhận vào DataFrame toàn thành phố và tên một quận mục tiêu (chẳng hạn "Providencia" hoặc "Santiago"). Hàm tự động tính toán và trả về một từ điển tổng kết hồ sơ quận gồm các khóa: quan, so_cho_o, ty_trong, gia_trung_vi, ty_le_nguyen_can. Hãy chạy thử nghiệm và so sánh hồ sơ giữa các quận.
Lời giải
Cách 1: Thiết kế hàm pipeline tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import numpy as np
import pandas as pd
def profile_district_pipeline(df: pd.DataFrame, ten_quan: str) -> dict:
sub = df[df["neighbourhood"] == ten_quan]
n_sub = len(sub)
n_total = len(df)
if n_sub == 0:
return {
"quan": ten_quan,
"so_cho_o": 0,
"ty_trong": 0.0,
"gia_trung_vi": np.nan,
"ty_le_nguyen_can": 0.0,
}
return {
"quan": ten_quan,
"so_cho_o": n_sub,
"ty_trong": round(float(n_sub / n_total), 3) if n_total > 0 else 0.0,
"gia_trung_vi": float(sub["price"].median()),
"ty_le_nguyen_can": round(
float((sub["room_type"] == "Entire home/apt").mean()), 3
),
}
display(pd.DataFrame([profile_district_pipeline(DEMO, "Providencia")]))
# Kết thúc lời giảiGiải thích cách 1
Hàm nhận tên quận qua ten_quan thay vì gắn cố định một tên trong thân hàm. Sau lọc, n_sub là số dòng quận và n_total là số dòng toàn bảng. Nhánh n_sub == 0 trả quy mô và các tỷ lệ bằng 0.0, còn trung vị là NaN để biểu thị chưa có giá quan sát được.
Với Providencia, hai mức giá 80000 và 30000 cho trung vị 55000. Hai dòng trên mười hai dòng toàn mẫu cho tỷ trọng khoảng 0.167 sau làm tròn. Một trong hai dòng là nguyên căn nên tỷ lệ nguyên căn bằng 0.5. Từ điển có tên trường giúp ghép nhiều lời gọi thành một DataFrame so sánh quận.
Kết quả cách 1
| quan | so_cho_o | ty_trong | gia_trung_vi | ty_le_nguyen_can |
|---|---|---|---|---|
| Providencia | 2 | 0.167 | 55000.0 | 0.5 |
Cách 2: Thiết kế pipeline với các phương thức vector hóa trên Series
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import numpy as np
import pandas as pd
def profile_district_pipeline(df: pd.DataFrame, ten_quan: str) -> dict:
mask = df["neighbourhood"].eq(ten_quan)
sub = df.loc[mask]
n_sub = len(sub)
if n_sub == 0:
return {
"quan": ten_quan,
"so_cho_o": 0,
"ty_trong": 0.0,
"gia_trung_vi": np.nan,
"ty_le_nguyen_can": 0.0,
}
return {
"quan": ten_quan,
"so_cho_o": n_sub,
"ty_trong": round(float(mask.mean()), 3),
"gia_trung_vi": float(sub["price"].median()),
"ty_le_nguyen_can": round(
float(sub["room_type"].eq("Entire home/apt").mean()), 3
),
}
display(pd.DataFrame([profile_district_pipeline(DEMO, "Providencia")]))
# Kết thúc lời giảiGiải thích cách 2
Cách thứ hai tạo mặt nạ bằng .eq(ten_quan), rồi dùng .loc[mask] lấy bảng quận. Khi quận tồn tại, mask.mean() tính trực tiếp phần số dòng được chọn trên toàn bảng không thiếu tên quận. Việc tính trung vị và cơ cấu sau đó chỉ dùng sub.
Nhánh rỗng nằm trước phép mean nên tên quận không có trong dữ liệu nhận kết quả quy ước rõ ràng. Để so sánh thêm một quận, chỉ cần đổi đối số trong lời gọi, không sửa công thức. Tuy nhiên gói nhiều bước thành một hàm chưa làm chúng chạy tự động cho mọi quận. Nếu cần hồ sơ tất cả quận, có thể thiết kế phép tổng hợp theo nhóm riêng.
Kết quả cách 2
| quan | so_cho_o | ty_trong | gia_trung_vi | ty_le_nguyen_can |
|---|---|---|---|---|
| Providencia | 2 | 0.167 | 55000.0 | 0.5 |
So sánh hai cách
Cách 1 dùng số dòng để tính tỷ trọng, Cách 2 dùng trung bình mặt nạ. Chúng giữ cùng câu hỏi và các trường kết quả. Tỷ trọng 0.167 mô tả DEMO, nên không được lẫn với các số đối chiếu của snapshot thực tế ở đầu lab.
Bài tập 11 (mở rộng 2): Tìm quận quy mô lớn có giá trung vị thấp nhất bằng groupby
Bài tập 11: Khảo sát nhóm thị trường quy mô lớn bằng groupby
Tìm quận có giá thuê trung vị thấp nhất trong số các quận đạt ngưỡng quy mô. Dùng ngưỡng ít nhất 3 chỗ ở trên DEMO và ít nhất 300 chỗ ở khi mở rộng sang snapshot Santiago. Ngưỡng xét số dòng chỗ ở, kể cả dòng thiếu giá.
Hãy viết hàm find_affordable_popular_district(df: pd.DataFrame, min_listings: int = 3) -> tuple sử dụng phương thức groupby("neighbourhood")["price"].agg(["median", "size"]). Hàm trả về một bộ giá trị gồm (tên_quận, giá_trung_vị, số_chỗ_ở). Sau đó, hãy kiểm chứng lại kết quả bằng cách lọc trực tiếp dữ liệu của quận đó.
Lời giải
Cách 1: Sử dụng chuỗi phương thức groupby, agg và lọc điều kiện
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def find_affordable_popular_district(df: pd.DataFrame, min_listings: int = 3) -> tuple:
# 1. Nhóm theo quận và tính đồng thời trung vị và quy mô
summary = df.groupby("neighbourhood")["price"].agg(["median", "size"])
# 2. Lọc các quận có quy mô đạt yêu cầu tối thiểu
filtered = summary[summary["size"] >= min_listings]
# 3. Lấy quận có giá trung vị thấp nhất
cheapest_row = filtered.nsmallest(1, "median").iloc[0]
return cheapest_row.name, float(cheapest_row["median"]), int(cheapest_row["size"])
ket_qua = find_affordable_popular_district(DEMO, min_listings=3)
display(pd.DataFrame([ket_qua], columns=["quan", "gia_trung_vi", "so_cho_o"]))
# Kết thúc lời giảiGiải thích cách 1
groupby("neighbourhood")["price"].agg(["median", "size"]) tạo một dòng cho mỗi quận với hai phép tổng hợp. Median bỏ giá thiếu, còn size đếm mọi dòng kể cả dòng thiếu giá. Điều này bám đúng yêu cầu ngưỡng số chỗ ở, không phải ngưỡng số giá hợp lệ.
Sau lọc size từ 3 trở lên, Providencia có hai dòng bị loại. Ñuñoa có sáu dòng và trung vị 45000, Santiago có bốn dòng và trung vị 40000. nsmallest(1, "median") lấy dòng có trung vị thấp nhất. .iloc[0] đọc dòng kết quả rồi hàm trả tên ở row.name cùng hai thống kê dưới dạng tuple.
Kết quả cách 1
| quan | gia_trung_vi | so_cho_o |
|---|---|---|
| Santiago | 40000.0 | 4 |
Cách 2: Sử dụng named aggregation kết hợp truy vấn query()
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def find_affordable_popular_district(df: pd.DataFrame, min_listings: int = 3) -> tuple:
# Gom nhóm với tên cột kết quả được đặt tường minh
grouped = (
df.groupby("neighbourhood")
.agg(
gia_trung_vi=("price", "median"),
so_cho_o=("price", "size"),
)
.query("so_cho_o >= @min_listings")
.sort_values(by="gia_trung_vi", ascending=True)
)
first_district = grouped.iloc[0]
return (
first_district.name,
float(first_district["gia_trung_vi"]),
int(first_district["so_cho_o"]),
)
ket_qua = find_affordable_popular_district(DEMO, min_listings=3)
display(pd.DataFrame([ket_qua], columns=["quan", "gia_trung_vi", "so_cho_o"]))
# Kết thúc lời giảiGiải thích cách 2
Named aggregation đặt tên cột kết quả ngay trong lời gọi agg. gia_trung_vi=("price", "median") xác định cột nguồn và phép tính, còn so_cho_o=("price", "size") xác định số dòng. .query("so_cho_o >= @min_listings") đọc biến Python min_listings nhờ dấu @.
Hàm xếp các nhóm đạt ngưỡng theo trung vị tăng dần rồi lấy nhóm đầu. Đây là phép sắp xếp toàn bộ nhóm đạt điều kiện, trong khi Cách 1 chỉ yêu cầu một cực trị. Cả hai đoạn mã hiện giả định có ít nhất một nhóm đạt ngưỡng và có trung vị xác định. Nếu không có, cần thiết kế thêm kết quả rỗng thay vì truy cập .iloc[0].
Kết quả cách 2
| quan | gia_trung_vi | so_cho_o |
|---|---|---|
| Santiago | 40000.0 | 4 |
So sánh hai cách
Hai cách giữ đúng khác biệt giữa quy mô nhóm và số giá hợp lệ. Cách 1 dùng nsmallest để tìm một nhóm, Cách 2 đặt tên cột rõ hơn và tạo thứ tự đầy đủ. Với dữ liệu mẫu, lọc lại Santiago cho bốn dòng và ba giá hợp lệ 40000, 12000, 95000, xác nhận trung vị 40000.
Bài tập 12 (mở rộng 3): Phân tích bản chất bộ nhớ và khắc phục bẫy gán chuỗi (ChainedAssignmentError)
Bài tập 12: Phân tích cơ chế gán chuỗi hai lần ngoặc vuông
Một đoạn mã do công cụ AI sinh ra để giảm
python
# Trích xuất bảng con quận Ñuñoa từ dữ liệu DEMO
nu = DEMO[DEMO["neighbourhood"] == "Ñuñoa"]
bao_gia = nu[["id", "room_type", "price"]].copy()
bao_gia["price"][bao_gia["room_type"] == "Entire home/apt"] *= 0.9Khi chạy đoạn mã trên trên pandas 3 (bật Copy-on-Write mặc định), hệ thống đưa ra cảnh báo hoặc kích hoạt ChainedAssignmentError, và kiểm tra lại thì thấy bảng bao_gia hoàn toàn không thay đổi giá trị.
- Hãy giải thích bản chất kiến trúc bộ nhớ vì sao bảng
bao_giakhông đổi. - Viết hàm
safe_apply_discount(df: pd.DataFrame, room_type: str, giam: float) -> pd.DataFramekhắc phục lỗi này bằng hai phương pháp chuẩn mực.
Lời giải
Cách 1: Sử dụng phương thức .loc gán đơn tầng
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import pandas as pd
def safe_apply_discount(df: pd.DataFrame, room_type: str, giam: float) -> pd.DataFrame:
# 1. Tạo bản sao dữ liệu an toàn
out = df.copy()
# 2. Sử dụng .loc để gán giá trị tại đúng tọa độ hàng và cột trong một bước duy nhất
mask = out["room_type"] == room_type
out.loc[mask, "price"] = out.loc[mask, "price"] * (1.0 - giam)
return out
display(safe_apply_discount(DEMO, "Entire home/apt", 0.1)[["id", "price"]])
# Kết thúc lời giảiGiải thích cách 1
Chuỗi gán hai lần chọn bao_gia["price"][mask] làm việc qua một Series trung gian. Trong cơ chế Copy-on-Write của pandas 3, sửa đối tượng trung gian không cập nhật bảng cha theo cách đó. .copy() khi tạo bao_gia không biến phép gán chuỗi thành một phép gán hợp lệ vào bảng.
Hàm sửa bằng .loc[mask, "price"] ở ngay bảng đích out. Nhờ vậy phép gán xác định cả hàng lẫn cột của đối tượng cần sửa trong một bước. out = df.copy() giữ bảng đầu vào nguyên vẹn. Trên toàn DEMO, sáu nguyên căn có giá được nhân 0.9, còn các loại khác giữ nguyên. Giá thiếu vẫn không có giá để báo.
Kết quả cách 1
| index | id | price |
|---|---|---|
| 0 | 101 | 40500.0 |
| 1 | 102 | 18000.0 |
| 2 | 103 | 64800.0 |
| 3 | 104 | NaN |
| 4 | 105 | 2500.0 |
| 5 | 106 | 54000.0 |
| 6 | 107 | 72000.0 |
| 7 | 108 | 30000.0 |
| 8 | 109 | 36000.0 |
| 9 | 110 | 12000.0 |
| 10 | 111 | NaN |
| 11 | 112 | 95000.0 |
Cách 2: Sử dụng phép toán vector hóa np.where
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112],
"name": [
"Depto Plaza Ñuñoa",
"Pieza cerca metro",
"Loft Irarrázaval",
"Depto sin price",
"Dormitorio simple",
"Casa familiar",
"Studio Providencia",
"Pieza Providencia",
"Depto Centro",
"Hostal Centro",
"Pieza Centro",
"Hotel boutique",
],
"neighbourhood": ["Ñuñoa"] * 6 + ["Providencia"] * 2 + ["Santiago"] * 4,
"room_type": [
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Entire home/apt",
"Private room",
"Entire home/apt",
"Shared room",
"Private room",
"Hotel room",
],
"price": [
45000.0,
18000.0,
72000.0,
np.nan,
2500.0,
60000.0,
80000.0,
30000.0,
40000.0,
12000.0,
np.nan,
95000.0,
],
"number_of_reviews": [12, 3, 0, 1, 25, 7, 4, 0, 30, 2, 5, 9],
}
)
# Bắt đầu lời giải
import numpy as np
import pandas as pd
def safe_apply_discount(df: pd.DataFrame, room_type: str, giam: float) -> pd.DataFrame:
out = df.copy()
mask = out["room_type"].eq(room_type)
# Thay thế giá trị dựa trên điều kiện không thông qua lát cắt trung gian
out["price"] = np.where(mask, out["price"] * (1.0 - giam), out["price"])
return out
display(safe_apply_discount(DEMO, "Entire home/apt", 0.1)[["id", "price"]])
# Kết thúc lời giảiGiải thích cách 2
np.where(mask, out["price"] * (1.0 - giam), out["price"]) chọn theo từng vị trí giữa hai nhánh. Vị trí True lấy giá giảm, vị trí False lấy giá cũ. Kết quả là một ndarray cùng độ dài, được gán lại cho toàn cột price của out, nên không có đối tượng con làm đích ghi.
Hai nhánh giá được tính trước khi np.where chọn. Với dữ liệu float64 của bài, cách này giữ NaN và thứ tự vị trí dòng. NumPy làm việc theo vị trí, không tự căn nhãn Series như nhiều phép toán pandas. Vì vậy mặt nạ và cả hai mảng giá phải được tạo từ đúng cùng bảng, cùng thứ tự. Nếu mở rộng sang dtype nullable, cần kiểm tra thêm cách chuyển dtype và dữ liệu thiếu.
Kết quả cách 2
| index | id | price |
|---|---|---|
| 0 | 101 | 40500.0 |
| 1 | 102 | 18000.0 |
| 2 | 103 | 64800.0 |
| 3 | 104 | NaN |
| 4 | 105 | 2500.0 |
| 5 | 106 | 54000.0 |
| 6 | 107 | 72000.0 |
| 7 | 108 | 30000.0 |
| 8 | 109 | 36000.0 |
| 9 | 110 | 12000.0 |
| 10 | 111 | NaN |
| 11 | 112 | 95000.0 |
So sánh hai cách
Cách 1 cập nhật những ô được chọn bằng loc và giữ cách diễn đạt theo nhãn của pandas. Cách 2 dựng toàn bộ cột mới bằng phép chọn của NumPy rồi gán một lần. Cả hai khắc phục gán chuỗi trên bảng mẫu và không đổi DEMO, nhưng không nên gọi Cách 2 nhanh hơn nếu chưa đo cả quy trình.
4. Bảng tổng kết kỹ thuật cốt lõi
| Khái niệm và thao tác | Cú pháp chuẩn mực | Lợi ích kỹ thuật | Ứng dụng ở các bài sau |
|---|---|---|---|
| Thói quen 5 bước | .shape, .sample(), .info(), .describe(), .value_counts() | Nắm bắt quy mô, kiểu dữ liệu, mức độ khuyết và phân bố dữ liệu trong 30 giây đầu tiên. | Đánh giá ban đầu mọi tập dữ liệu mới và kiểm chứng kết quả do AI sinh ra. |
| Trích xuất cột và hàng | df["cột"], df[["a", "b"]], df[mask], df.loc[mask, "cột"] | Phân biệt kiểu trả về Series và DataFrame. Chọn đúng hàng và cột khi cần cập nhật giá trị. | Toàn bộ các thao tác xử lý dữ liệu từ Bài 5 trở đi. |
| Lọc kết hợp đa điều kiện | (cond1) & (cond2) | Kết hợp các điều kiện logic trên mảng boolean bằng toán tử bitwise với cặp ngoặc đơn bắt buộc. | Phân đoạn khách hàng, lọc bản ghi theo tiêu chí phức tạp. |
| Top phần tử | df.nsmallest(n, "cột") | Thuật toán heap hiệu năng cao, bảo toàn chỉ mục và tự động xử lý giá trị khuyết NaN. | Tìm top sản phẩm, chỗ ở giá rẻ, bài toán xếp hạng. |
| Cập nhật dữ liệu an toàn | df.loc[mask, "cột"] = giá_trị | Cập nhật trực tiếp vào bộ nhớ bảng gốc, loại bỏ hoàn toàn bẫy gán chuỗi ChainedAssignmentError. | Khâu làm sạch dữ liệu (Bài 10) và tiền xử lý biến đặc trưng. |
| Ảnh hưởng của mẫu số | .dropna() trước khi tính tỷ lệ | Tránh ngộ nhận do phép so sánh NaN trả về False làm thổi phồng mẫu số giả tạo. | Thống kê tỷ lệ chuyển đổi, tỷ lệ lỗi trong bài toán thực tế. |
| Đóng gói và xuất bản | df.to_csv("path.csv", index=False) | Tạo tệp dữ liệu phẳng chuẩn hóa, không thừa cột chỉ mục, kiểm chứng bất biến khi nạp lại. | Xuất kết quả phân tích, kết nối đường ống dữ liệu (pipeline). |
5. Tài liệu tham khảo
- Wes McKinney (2022). Python for Data Analysis: Data Wrangling with pandas, NumPy, and Jupyter (3rd Edition). O'Reilly Media. Chương 5: Getting Started with pandas.
- pandas Development Team (2024–2026). pandas Documentation: Indexing and selecting data & Copy-on-Write User Guide. Truy cập tại:
https://pandas.pydata.org/docs/. - Inside Airbnb (2026). Santiago de Chile Data Snapshot (2026-06-29). Lưu trữ mã nguồn mở phục vụ nghiên cứu và phân tích dữ liệu đô thị.
Khởi động pandas: Truy xuất theo nhãn, vị trí, căn chỉnh tự động và phân loại dải giá
Một nhãn chỉ mục và một vị trí hàng có thể trông giống nhau nhưng chọn ra kết quả khác. Ba bài sau luyện cách truy xuất, căn chỉnh hai bảng giá và chia dải giá có ranh giới rõ ràng.
1. Thiết lập môi trường và dữ liệu thực hành
Để bảo đảm toàn bộ quy trình thực hành có thể chạy độc lập, phần mở đầu sử dụng ba cấu trúc dữ liệu nhỏ gồm một chuỗi chỉ mục không liên tục, hai chuỗi khảo sát giá qua hai thời điểm và bảng dữ liệu mẫu 12 dòng (đơn vị: CLP/đêm). Bộ dữ liệu nhỏ này phản ánh đầy đủ các đặc tính của dữ liệu thực tế: Đa dạng loại phòng, chứa dữ liệu khuyết (NaN) và có các mức giá ở nhiều dải khác nhau.
python
import numpy as np
import pandas as pd
# Chuỗi dữ liệu với chỉ mục số nguyên không liên tục phục vụ phân biệt nhãn và vị trí
s_demo = pd.Series([100, 200, 300], index=[2, 0, 5])
# Dữ liệu khảo sát giá qua hai thời điểm phục vụ bài toán căn chỉnh tự động
gia_cu = pd.Series({"A": 100.0, "B": 200.0, "C": 50.0})
gia_moi = pd.Series({"B": 220.0, "A": 90.0, "D": 70.0})
# Bảng dữ liệu mẫu 12 dòng (đơn vị: CLP/đêm)
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"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],
}
)
# Đường dẫn dữ liệu thực tế Inside Airbnb phục vụ đối chiếu quy mô lớn
SNAPSHOT_URL = (
"https://data.insideairbnb.com/chile/rm/santiago/"
"2026-06-29/visualisations/listings.csv"
)NOTE
Đối chiếu thực tế: Khi đối chiếu trên tập dữ liệu đầy đủ của vùng đô thị Santiago (18.534 dòng dữ liệu niêm yết), thị trường có đúng 846 phòng khuyết giá. Việc phân đoạn giá trên dữ liệu thực tế tạo ra ba dải quy mô: Phân khúc trung bình có 12.325 phòng, phân khúc cao có 3.703 phòng và phân khúc rẻ có 1.660 phòng. Các số liệu thực tế này được đưa vào để đối chiếu tính tổng quát của giải thuật, không dùng làm hằng số gán cứng trong mã nguồn.
2. Nội dung chi tiết các bài tập thực hành
Bài tập 1: Phân biệt truy xuất theo nhãn với loc và theo vị trí với iloc
Bài tập 1: Phân biệt loc và iloc trên Series
Cho một đối tượng pd.Series tên là s có chỉ mục không trùng lặp, một giá trị nhãn nhan chắc chắn tồn tại trong chỉ mục và một chỉ số nguyên vi_tri hợp lệ biểu diễn vị trí hàng (có thể nhận giá trị âm như -1).
Hãy viết hàm loc_iloc(s: pd.Series, nhan, vi_tri: int) -> dict trả về một từ điển gồm đúng hai khóa sau:
theo_nhan: Giá trị của phần tử mang nhãnnhan, truy xuất thông qua thuộc tính.loc.theo_vi_tri: Giá trị của phần tử ở vị trívi_tri, truy xuất thông qua thuộc tính.iloc.
Yêu cầu kỹ thuật: Bắt buộc sử dụng .loc và .iloc, tuyệt đối không dùng cú pháp truy cập trực tiếp s[...] để tránh nhập nhằng khi chỉ mục mang kiểu số nguyên.
Lời giải
Cách 1: Tách từng bước truy xuất theo nhãn và theo vị trí
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
s_demo = pd.Series([100, 200, 300], index=[2, 0, 5])
# Bắt đầu lời giải
import pandas as pd
def loc_iloc(s: pd.Series, nhan, vi_tri: int) -> dict:
# 1. Truy xuất phần tử theo giá trị nhãn của chỉ mục bằng .loc
gia_tri_nhan = s.loc[nhan]
# 2. Truy xuất phần tử theo vị trí thứ tự thực tế bằng .iloc
gia_tri_vi_tri = s.iloc[vi_tri]
# 3. Đóng gói kết quả vào từ điển chuẩn đầu ra
return {
"theo_nhan": gia_tri_nhan,
"theo_vi_tri": gia_tri_vi_tri,
}
# Gọi thử nghiệm hàm với chuỗi dữ liệu mẫu:
ket_qua_q1_c1 = loc_iloc(s_demo, 2, 2)
print("Kết quả Cách 1:", ket_qua_q1_c1)
# Kết thúc lời giảiGiải thích Cách 1
s.loc[nhan] tra nhãn trong chỉ mục. Nhãn 2 nằm ở dòng đầu của s_demo, nên giá trị nhận được là 100. s.iloc[vi_tri] đếm vị trí từ 0. Vị trí 2 là dòng thứ ba, có giá trị 300. Hai số 2 giống nhau về cách viết nhưng giữ hai vai trò khác nhau.
Các biến gia_tri_nhan và gia_tri_vi_tri lưu hai kết quả trước khi return ghép chúng vào một từ điển. return kết thúc lời gọi hàm và giao từ điển cho nơi gọi. Với chỉ mục duy nhất như đề đã giả thiết, mỗi phép truy xuất trả về một giá trị đơn. Nếu bỏ giả thiết này, .loc có thể trả về nhiều phần tử.
.iloc[-1] chọn phần tử cuối, còn .loc[-1] yêu cầu nhãn -1 thật sự tồn tại. Vị trí vượt phạm vi gây IndexError, nhãn không tồn tại gây KeyError. Không cần tự xử lý hai trường hợp ấy trong bài vì đầu vào đã được bảo đảm hợp lệ.
Output Cách 1
| Khóa trả về | Phép truy xuất | Giá trị |
|---|---|---|
theo_nhan | s_demo.loc[2] | 100 |
theo_vi_tri | s_demo.iloc[2] | 300 |
Cách 2: Trả trực tiếp từ điển kết quả
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
s_demo = pd.Series([100, 200, 300], index=[2, 0, 5])
# Bắt đầu lời giải
import pandas as pd
def loc_iloc(s: pd.Series, nhan, vi_tri: int) -> dict:
# Trích xuất trực tiếp vào từ điển kết quả mà không cần biến trung gian
return {
"theo_nhan": s.loc[nhan],
"theo_vi_tri": s.iloc[vi_tri],
}
# Gọi thử nghiệm hàm với nhãn 0 và vị trí 0:
ket_qua_q1_c2 = loc_iloc(s_demo, 0, 0)
print("Kết quả Cách 2:", ket_qua_q1_c2)
# Kết thúc lời giảiGiải thích Cách 2
Hai phép truy xuất được đặt ngay trong biểu thức từ điển. Python tính từng giá trị rồi tạo từ điển có đúng hai khóa. Việc bỏ các biến trung gian không thay đổi quy tắc tìm nhãn hoặc đếm vị trí của pandas.
Lời gọi minh họa thứ hai dùng nhan=0 và vi_tri=0. Nhãn 0 nằm ở dòng thứ hai nên .loc[0] trả 200. Vị trí 0 là dòng đầu nên .iloc[0] trả 100. Đừng dùng kết quả khác nhau của hai ví dụ để kết luận hai phiên bản hàm khác nhau về hành vi. Khi truyền cùng ba đối số, chúng trả cùng hai giá trị.
Output Cách 2
| Khóa trả về | Phép truy xuất | Giá trị |
|---|---|---|
theo_nhan | s_demo.loc[0] | 200 |
theo_vi_tri | s_demo.iloc[0] | 100 |
So sánh hai cách
Cách 1 thuận tiện khi đặt điểm dừng để xem từng giá trị. Cách 2 phù hợp với hàm ngắn đã rõ ý nghĩa. Cả hai thực hiện đúng hai lần truy xuất, nên không có cơ sở để coi cách viết ngắn hơn là một cải tiến thuật toán. Trong một số phiên bản NumPy, print hiển thị giá trị trong từ điển dưới dạng np.int64(100) thay vì 100. Kiểu vô hướng NumPy này không làm thay đổi giá trị trong bảng.
Bài tập 2: Cơ chế căn chỉnh tự động theo index khi tính toán số học
Bài tập 2: Phép toán số học tự khớp theo nhãn chỉ mục
Cho hai đối tượng pd.Series số thực gia_cu và gia_moi, mỗi chuỗi đều có chỉ mục là tập hợp các nhãn duy nhất không trùng lặp.
Hãy viết hàm align_change(gia_cu: pd.Series, gia_moi: pd.Series) -> dict tính toán mức chênh lệch giá và trả về một từ điển gồm đúng hai khóa sau:
thay_doi: Đối tượngpd.Serieskết quả của phép toán trừgia_moi - gia_cu. Chỉ mục của Series này là hợp của hai chỉ mục ban đầu.so_nan: Một số nguyên (int) đếm chính xác số lượng giá trịNaNxuất hiện trong Seriesthay_doi.
Yêu cầu kỹ thuật: Không sử dụng vòng lặp thủ công, không làm thay đổi dữ liệu của hai Series đầu vào.
Lời giải
Cách 1: Trừ trực tiếp hai Series rồi đếm ô khuyết
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
gia_cu = pd.Series({"A": 100.0, "B": 200.0, "C": 50.0})
gia_moi = pd.Series({"B": 220.0, "A": 90.0, "D": 70.0})
# Bắt đầu lời giải
import pandas as pd
def align_change(gia_cu: pd.Series, gia_moi: pd.Series) -> dict:
# 1. Thực hiện phép trừ, pandas tự động căn chỉnh theo nhãn chỉ mục
thay_doi = gia_moi - gia_cu
# 2. Đếm số lượng ô mang giá trị khuyết NaN
so_nan = int(thay_doi.isna().sum())
# 3. Đóng gói kết quả trả về
return {
"thay_doi": thay_doi,
"so_nan": so_nan,
}
# Gọi thử nghiệm hàm với hai chuỗi giá khảo sát mẫu:
ket_qua_q2_c1 = align_change(gia_cu, gia_moi)
print("Kết quả Cách 1:")
print(ket_qua_q2_c1["thay_doi"])
print("Số ô NaN:", ket_qua_q2_c1["so_nan"])
# Kết thúc lời giảiGiải thích Cách 1
Phép trừ gia_moi - gia_cu tìm các phần tử có cùng nhãn trước khi tính. Ở nhãn A, phép tính là 90 - 100, cho -10. Ở nhãn B, phép tính là 220 - 200, cho 20. Thứ tự B rồi A trong gia_moi không khiến chúng bị trừ nhầm theo vị trí.
Chỉ mục kết quả chứa cả A, B, C và D. C thiếu giá mới, D thiếu giá cũ nên hai phép tính ấy chưa có đủ dữ kiện và trả NaN. .isna() tạo một Series boolean đánh dấu ô khuyết. .sum() cộng các giá trị True như 1, rồi int(...) chuyển số đếm thành số nguyên Python theo yêu cầu đầu ra.
Hàm chỉ tạo kết quả tính toán mới và không gán ngược vào hai Series nguồn. Khi nhãn đại diện cho mã sản phẩm hoặc chỗ ở, việc giữ chỉ mục là điều giúp phép tính đi đúng đối tượng.
Output Cách 1
| Nhãn | thay_doi |
|---|---|
| A | -10.0 |
| B | 20.0 |
| C | NaN |
| D | NaN |
so_nan = 2.
Cách 2: Dùng phương thức sub để diễn đạt phép trừ
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
gia_cu = pd.Series({"A": 100.0, "B": 200.0, "C": 50.0})
gia_moi = pd.Series({"B": 220.0, "A": 90.0, "D": 70.0})
# Bắt đầu lời giải
import pandas as pd
def align_change(gia_cu: pd.Series, gia_moi: pd.Series) -> dict:
# Sử dụng phương thức .sub() của đối tượng Series
thay_doi = gia_moi.sub(gia_cu)
# Trích xuất số lượng ô khuyết thông qua tổng mảng boolean
so_nan = int(thay_doi.isna().sum())
return {
"thay_doi": thay_doi,
"so_nan": so_nan,
}
# Gọi thử nghiệm hàm với hai chuỗi giá khảo sát mẫu:
ket_qua_q2_c2 = align_change(gia_cu, gia_moi)
print("Kết quả Cách 2:")
print(ket_qua_q2_c2["thay_doi"])
print("Số ô NaN:", ket_qua_q2_c2["so_nan"])
# Kết thúc lời giảiGiải thích Cách 2
gia_moi.sub(gia_cu) là cách gọi phương thức cho cùng phép trừ có căn chỉnh. Đối tượng đứng trước .sub là số bị trừ, đối số truyền vào là số trừ. Vì vậy không được đổi vị trí hai Series nếu vẫn muốn tính giá mới trừ giá cũ.
Sau phép trừ, chuỗi .isna().sum() vẫn đếm hai nhãn chưa có đủ hai mức giá. .sub còn nhận fill_value trong những bài toán có quy ước bù dữ liệu. Bài này không truyền đối số đó vì thiếu một mức giá chưa có nghĩa mức giá ấy bằng 0.
Output Cách 2
| Nhãn | thay_doi |
|---|---|
| A | -10.0 |
| B | 20.0 |
| C | NaN |
| D | NaN |
so_nan = 2.
So sánh hai cách
Cả hai cách có cùng quy tắc căn chỉnh và cùng kết quả. Toán tử - dễ đọc khi chỉ có một phép tính. .sub() tiện khi viết một chuỗi phương thức hoặc cần cấu hình rõ cách bù giá trị khuyết. Thêm fill_value=0 sẽ đổi ý nghĩa bài toán, nên không thể xem đó là một bước tối ưu giữ nguyên kết quả.
Bài tập 3: Phân loại dải giá bằng map với hàm tự định nghĩa
Bài tập 3: Phân chia phân khúc giá phòng bằng map
Trong nghiên cứu thị trường lưu trú, các mức giá niêm yết được phân chia thành ba phân khúc:
- Phân khúc rẻ (
"re"): Giá phòng nhỏ hơn 30.000 CLP. - Phân khúc trung bình (
"trung"): Giá phòng từ 30.000 CLP đến dưới 100.000 CLP. - Phân khúc cao (
"cao"): Giá phòng từ 100.000 CLP trở lên.
Hãy viết hàm phan_khuc(gia) -> str | None nhận vào một giá trị số thực đại diện cho giá niêm yết (hoặc giá trị khuyết np.nan, None). Hàm trả về chuỗi tương ứng "re", "trung", "cao", hoặc trả về None nếu giá trị đầu vào bị khuyết.
Yêu cầu kỹ thuật: Phải kiểm tra giá trị khuyết bằng pd.isna trước khi so sánh ngưỡng số học để tránh lỗi so sánh kiểu dữ liệu. Sau đó, áp dụng hàm này trên cột giá của bảng dữ liệu thông qua phương thức map.
Lời giải
Cách 1: Viết hàm phân loại một giá rồi áp dụng bằng map
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)),
"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],
}
)
# Bắt đầu lời giải
import numpy as np
import pandas as pd
def phan_khuc(gia):
# Bước 1: Kiểm tra ô khuyết trước tiên bằng pd.isna
if pd.isna(gia):
return None
# Bước 2: Kiểm tra các ngưỡng giá theo thứ tự tăng dần
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
else:
return "cao"
# Gọi thử nghiệm hàm với các trường hợp đơn lẻ:
print("Giá 25.000 CLP:", phan_khuc(25000.0))
print("Giá 30.000 CLP:", phan_khuc(30000.0))
print("Giá 99.999 CLP:", phan_khuc(99999.0))
print("Giá 100.000 CLP:", phan_khuc(100000.0))
print("Giá khuyết NaN:", phan_khuc(np.nan))
# Áp dụng hàm trên toàn bộ cột price của bảng DEMO:
phan_khuc_demo = DEMO["price"].map(phan_khuc)
print("Phân bố phân khúc trên 12 dòng DEMO:")
print(phan_khuc_demo.value_counts(dropna=False))
# Kết thúc lời giảiGiải thích Cách 1
pd.isna(gia) phải được xét trước các ngưỡng để ô khuyết trả None. Nếu để np.nan đi qua các phép so sánh <, nó không rơi vào hai nhánh đầu và có thể bị gán nhầm nhãn cao. Với pd.NA, dùng trực tiếp kết quả so sánh làm điều kiện còn có thể gây lỗi vì trạng thái đúng sai chưa xác định.
Sau khi loại trường hợp khuyết, hai nhánh kiểm tra theo thứ tự tăng dần. Giá 30000 không nhỏ hơn 30000, nhưng nhỏ hơn 100000, nên nhận trung. Giá 100000 đi tới nhánh cuối và nhận cao.
DEMO["price"].map(phan_khuc) gọi hàm với từng giá và giữ chỉ mục của cột giá trong Series mới. .value_counts(dropna=False) đếm cả nhãn khuyết, giúp kiểm tra đủ 12 dòng. Đây là quy tắc phân dải theo đề, chưa phải bước kiểm tra giá âm hoặc giá bất hợp lý.
Output Cách 1
Các lời gọi đơn lẻ lần lượt trả re, trung, trung, cao, None cho các giá 25000, 30000, 99999, 100000, np.nan.
| Nhãn phân khúc | Số dòng trên DEMO |
|---|---|
re | 2 |
trung | 5 |
cao | 4 |
| Khuyết | 1 |
Cách 2: Dùng cut để phân khoảng cho cả cột
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)),
"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],
}
)
# Bắt đầu lời giải
import numpy as np
import pandas as pd
def phan_khuc(gia):
# Đóng gói hàm scalar để phục vụ kiểm tra đơn vị
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
if gia < 100000.0:
return "trung"
return "cao"
# Gọi thử hàm trên giá trị đơn lẻ:
print("Kiểm tra phân khúc giá 100.000 CLP:", phan_khuc(100000.0))
# Trong thực tế, phân đoạn hàng loạt trên Series được thực hiện tối ưu bằng pd.cut:
danh_muc_cut = pd.cut(
DEMO["price"],
bins=[-np.inf, 30000.0, 100000.0, np.inf],
right=False,
labels=["re", "trung", "cao"],
)
print("Kết quả vector hóa bằng pd.cut:")
print(danh_muc_cut.value_counts(dropna=False))
# Kết thúc lời giảiGiải thích Cách 2
Hàm phan_khuc vẫn cung cấp giao diện cho một giá trị đơn. Khi cần chia cả cột theo các khoảng cố định, pd.cut nhận Series giá cùng danh sách biên [-np.inf, 30000, 100000, np.inf]. Bốn biên tạo ba khoảng, do đó phải truyền đúng ba nhãn.
right=False đóng đầu trái và mở đầu phải của từng khoảng. Vì vậy 30000 thuộc trung, còn 100000 thuộc cao. Các giá khuyết tiếp tục khuyết. labels làm kết quả có kiểu phân loại category, khác với Series chuỗi do map tạo ra.
.value_counts(dropna=False) ở cuối chỉ dùng để xem phân bố. Nó thường sắp theo số lần xuất hiện, nên thứ tự in trung, cao, re không có nghĩa biên phân khoảng bị đổi.
Output Cách 2
| Nhãn phân khúc | Số dòng trên DEMO |
|---|---|
re | 2 |
trung | 5 |
cao | 4 |
| Khuyết | 1 |
So sánh hai cách
Cách 1 mở rõ từng nhánh và thuận tiện khi bổ sung quy tắc không thể diễn đạt bằng các khoảng số. Cách 2 mô tả trực tiếp bài toán phân khoảng cho cả cột và thường là lựa chọn phù hợp cho dữ liệu lớn. Muốn khẳng định nhanh hơn bao nhiêu phải đo trên dữ liệu và môi trường cụ thể. Khi đối chiếu hai kết quả, cần xét cả nhãn khuyết và sự khác nhau giữa kiểu chuỗi với kiểu category.
Tài liệu tham khảo
Wes McKinney. Python for Data Analysis: Data Wrangling with pandas, NumPy, and Jupyter, tái bản lần 3. O'Reilly Media, 2022. Chương 5: Getting Started with pandas.
The pandas Development Team. pandas documentation: Indexing and selecting data & Copy-on-Write. https://pandas.pydata.org/docs/user_guide/indexing.html
Viện Trí tuệ Nhân tạo, Trường Đại học Công nghệ, ĐHQGHN. Học liệu môn học Lập trình xử lý dữ liệu: Bài 4 - Làm quen với pandas và Bài 5 - Series & DataFrame chuyên sâu. Các bài tập thực hành trong bài giảng được biên soạn và chuẩn hóa dựa trên học liệu mở của IAI 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.