Không gian tự học dành cho sinh viên UET

Chào mừng đến với UETệ

Tìm bài giảng theo học phần, hiểu lại những kiến thức nền và ôn tập theo nhịp học của bạn.

Bài giảng
Đọc Notes, ôn nhanh với Slides và luyện tập qua các bài có lời giải.
Wiki kiến thức
Tra cứu thuật ngữ và bổ sung kiến thức nền ngay khi cần.
Góc học tập
Ôn bằng flashcard, lưu ghi chú và hẹn giờ tập trung.

Bạn có thể xem lại phần giới thiệu ở cuối trang.

Buy me a coffee

Website hoàn toàn miễn phí. Nếu thấy hữu ích, bạn có thể tặng tôi một cốc cà phê theo thông tin dưới đây nhé!

Mã QR chuyển khoản Momo
Ngân hàng
Momo
Tên người nhận
MOMO - TKTH DO MINH TRI
Số tài khoản
01MMTTT0060405229
Skip to content

Series, DataFrame và chỉ mục trong pandas

Bài 5Cập nhật 18/10/2026Ghi chú của bạ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ẽ:

  1. 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.
  2. 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: int64

Khá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 nhanh n dòng đầu tiên hoặc n 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.1

Xó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 operations

Tí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     0

Trong 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ừ 0 đến n−1:

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] == 20

Trong 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] = 50

Vớ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 s1+s2 sẽ lấy hợp của toàn bộ các nhãn:

  • Nhãn "a" chỉ có ở s1, thiếu ở s2⟹ kết quả là NaN.
  • Nhãn "b" có ở cả hai bên: 20+30=50.
  • Nhãn "c" chỉ có ở s2, thiếu ở s1⟹ 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.0

Phé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ếu axis=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ức applymap cũ).
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ừ 1 đến n cho các phần tử. Khi các giá trị bằng nhau, tham số method quyế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 1,5).
    • "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ị 25%, 50% (trung vị), 75%, 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ề 0,0. Để bắt buộc phép tính phải có ít nhất một giá trị hợp lệ mới sinh ra kết quả, ta sử dụng tham số 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 ​

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 đêmPhí dọn dẹp
a1000
b80100
c60300
  1. Tạo DataFrame cho_o biểu diễn bảng trên với chỉ mục là tên các chỗ ở ["a", "b", "c"].
  2. 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?
  3. Từ bao nhiêu đêm trở lên thì chỗ ở c sẽ có tổng chi phí rẻ hơn cả a và 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
  1. 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"])
  1. 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à:
    T(n)=pn+f.
    • Với chuyến đi 2 đêm (n=2):
      • Chỗ ở a: 100×2+0=200 USD.
      • Chỗ ở b: 80×2+100=260 USD.
      • Chỗ ở c: 60×2+300=420 USD.
      • Lựa chọn rẻ nhất là chỗ ở a.
    • Với chuyến đi 7 đêm (n=7):
      • Chỗ ở a: 100×7+0=700 USD.
      • Chỗ ở b: 80×7+100=660 USD.
      • Chỗ ở c: 60×7+300=720 USD.
      • Lựa chọn rẻ nhất là chỗ ở b.
  2. Xác định số đêm tối thiểu để c rẻ hơn cả a và b: Gọi n là số đêm lưu trú (n là số nguyên dương):
    • Để c rẻ hơn a:
      60n+300<100n,40n>300,n>7,5.
    • Để c rẻ hơn b:
      60n+300<80n+100,20n>200,n>10.
    Kết hợp hai điều kiện: n>10. Vì số đêm là số nguyên nên số đêm lưu trú tối thiểu để c trở thành phương án tiết kiệm nhất là 11 đêm. Kiểm tra số học:
    • Tại n=10: a =1000, b =900, c =900 (b và c bằng giá nhau).
    • Tại n=11: a =1100, b =980, c =960 (c rẻ 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 1Lần 2
a100110
b200210
c300Chưa có giá (khuyết thiếu)
  1. Tạo DataFrame df_gia và tính giá trung bình của các giá đã biết ở mỗi lần thu thập.
  2. 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ỗ ở a và b).
  3. 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}")
  1. Giải thích bản chất:
    • Ở cách tính thứ nhất, giá trung bình giảm từ 200 xuống 160 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 c với giá 300 USD kéo trung bình lên cao. Sang lần 2, căn hộ c bị 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 (a và b), giá phòng thực tế tăng từ 150 lên 160 USD (mỗi phòng đều tăng đúng 10 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ế.

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 ≤100 USD/đêm, còn nếu là loại "Phòng riêng" thì giá phải ≤60 USD/đêm.

  1. 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.
  2. 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 loai và gia.
Lời giải
  1. Phân tích từng dòng dữ liệu:
    • Hàng a: Loại "Nguyên căn", giá 90≤100⟹ Thỏa mãn.
    • Hàng b: Loại "Phòng riêng", giá 70>60⟹ Không thỏa mãn (mặc dù 70≤100 nhưng đây là phòng riêng).
    • Hàng c: Loại "Nguyên căn", giá 120>100⟹ Không thỏa mãn.
    • Hàng d: Loại "Phòng riêng", giá 60≤60⟹ 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 a và d.
  2. 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á 10% cho tất cả các phòng loại "Nguyên căn", sau đó tính cột tiền thuê cho ba đêm 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
  1. Đoạn mã trên cho giá trị cột ba_dem của phòng a bằ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?
  2. 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
  1. Đoạn mã trên cho giá trị ba_dem của phòng a là 300,0. Tuy nhiên, theo đúng yêu cầu nghiệp vụ, giá phòng a sau khi giảm 10% còn 90,0 USD, do đó tiền ba đêm phải là 90×3=270,0 USD.
  2. Hai lỗi logic:
    • Lỗi thứ tự tính toán: Đoạn mã tính toán cột ba_dem trướ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án gia_nguyen_can *= 0.9 chỉ biến đổi trên Series này mà không cập nhật ngược lại vào cột gia của bao_gia (do cơ chế bảo vệ Copy-on-Write).
  3. Đ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"] =90,0, bao_gia.loc["a", "ba_dem"] =270,0, trong khi phòng b giữ nguyên 50,0 và 150,0, còn phòng 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)
  1. 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.
  2. 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 20, P2 nhận 10 và P3 nhận 30, dẫn đến sai lệch dữ liệu nghiêm trọng.

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   20

pandas tự động căn chỉnh theo nhãn: Mã P1 nhận đúng phí 10, P2 nhận 30 và P3 nhận 20. Kể cả khi ta đảo thứ tự các hàng trong bảng 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ừ 7 đêm trở lên được miễn phí dọn dẹp hoàn toàn (phí bằng 0). Phí dọn dẹp chỉ thu một lần duy nhất cho cả lượt ở. Hãy tính tổng số tiền 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 <7): 100×2+20=220 USD.
  • Lượt P2 (7 đêm ≥7, miễn phí): 80×7+0=560 USD.
  • Lượt P3 (8 đêm ≥7, miễn phí): 60×8+0=480 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 ​

  1. 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.
  2. 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ức nsmallest.
  3. 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.
  4. 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.
  5. Đó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:

  1. cot_gia: Đối tượng Series chứa dữ liệu của cột price.
  2. bang_con: Đối tượng DataFrame gồm đúng hai cột name và price theo đúng thứ tự đó.
  3. 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.
  4. 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].
  5. gia_nguyen_can: Đối tượng Series chứa giá trị cột price củ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ải

Giả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 ​

indexnameprice
0Depto Plaza Ñuñoa45000.0
1Pieza cerca metro18000.0
2Loft Irarrázaval72000.0
3Depto sin priceNaN
4Dormitorio simple2500.0
5Casa familiar60000.0
6Studio Providencia80000.0
7Pieza Providencia30000.0
8Depto Centro40000.0
9Hostal Centro12000.0
10Pieza CentroNaN
11Hotel boutique95000.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ải

Giả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 ​

indexnameprice
0Depto Plaza Ñuñoa45000.0
1Pieza cerca metro18000.0
2Loft Irarrázaval72000.0
3Depto sin priceNaN
4Dormitorio simple2500.0
5Casa familiar60000.0
6Studio Providencia80000.0
7Pieza Providencia30000.0
8Depto Centro40000.0
9Hostal Centro12000.0
10Pieza CentroNaN
11Hotel boutique95000.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:

  1. Kiểm tra kích thước bảng qua thuộc tính .shape.
  2. Lấy mẫu ngẫu nhiên các dòng bằng .sample().
  3. Kiểm tra kiểu dữ liệu và số lượng giá trị hợp lệ bằng .info().
  4. Xem bảng tóm tắt thống kê mô tả phân phối giá bằng .describe().
  5. Đế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ột price.
  • so_loai_phong: Số lượng các loại hình phòng khác nhau xuất hiện trong cột room_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ải

Giả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_dongso_cotso_thieu_priceso_loai_phong
12624

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ải

Giả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_dongso_cotso_thieu_priceso_loai_phong
12624

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ảng df.

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ải

Giả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 ​

indexidroom_typeprice
0101Entire home/apt45000.0
1102Private room18000.0
2103Entire home/apt72000.0
3104Entire home/aptNaN
4105Private room2500.0
5106Entire home/apt60000.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ải

Giả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 ​

indexidroom_typeprice
0101Entire home/apt45000.0
1102Private room18000.0
2103Entire home/apt72000.0
3104Entire home/aptNaN
4105Private room2500.0
5106Entire home/apt60000.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:

  1. Cột room_type có giá trị đúng bằng tham số truyền vào.
  2. Cột price có giá trị nhỏ hơn hoặc bằng gia_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ải

Giả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ải

Giả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 n dòng có giá thấp nhất, chỉ giữ lại hai cột 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ải

Giả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 ​

indexnameprice
4Dormitorio simple2500.0
1Pieza cerca metro18000.0
0Depto Plaza Ñuñoa45000.0
5Casa familiar60000.0
2Loft Irarrázaval72000.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ải

Giả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 ​

indexnameprice
4Dormitorio simple2500.0
1Pieza cerca metro18000.0
0Depto Plaza Ñuñoa45000.0
5Casa familiar60000.0
2Loft Irarrázaval72000.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á 10% cho toàn bộ các chỗ ở thuộc loại hình nguyên căn (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 (0≤giam<1). Kết quả trả về là một DataFrame mới gồm đúng 4 cột theo thứ tự: id, room_type, price, tien_3_dem.

  • Cột price của các dòng thỏa mãn loại phòng được giảm theo biểu thức giá mới=giá cũ×(1−giam) bằ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 price×3, căn cứ trên mức giá sau khi đã giảm. Các ô có giá NaN vẫ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ải

Giả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 ​

idroom_typepricetien_3_dem
101Entire home/apt40500.0121500.0
102Private room18000.054000.0
103Entire home/apt64800.0194400.0
104Entire home/aptNaNNaN
105Private room2500.07500.0
106Entire home/apt54000.0162000.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ải

Giả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 ​

idroom_typepricetien_3_dem
101Entire home/apt40500.0121500.0
102Private room18000.054000.0
103Entire home/apt64800.0194400.0
104Entire home/aptNaNNaN
105Private room2500.07500.0
106Entire home/apt54000.0162000.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:

  1. 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().
  2. 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).
  3. so_thieu: Số lượng giá trị NaN xuất hiện trong chuỗi prices.

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ải

Giả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_thoty_le_sachso_thieu
0.3333330.41

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ải

Giả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_thoty_le_sachso_thieu
0.3333330.41

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:

  1. 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ức value_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 [0,1].
  2. 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 0 đến 100), đượ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ải

Giả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_typety_le
Entire home/apt0.666667
Private room0.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ải

Giả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_typety_le
Entire home/apt0.666667
Private room0.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:

  1. 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ỏ qua NaN).
    • 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.
  2. Ghi DataFrame này ra đĩa cứng dưới định dạng CSV bằng to_csv(path, index=False, encoding="utf-8").
  3. Đọ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ải

Giả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 ​

quanso_cho_ogia_trung_vity_le_nguyen_canty_le_thieu_gia_pct
Ñuñoa645000.00.66716.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ải

Giả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 ​

quanso_cho_ogia_trung_vity_le_nguyen_canty_le_thieu_gia_pct
Ñuñoa645000.00.66716.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ải

Giả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 ​

quanso_cho_oty_tronggia_trung_vity_le_nguyen_can
Providencia20.16755000.00.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ải

Giả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 ​

quanso_cho_oty_tronggia_trung_vity_le_nguyen_can
Providencia20.16755000.00.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ải

Giả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 ​

quangia_trung_viso_cho_o
Santiago40000.04

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ải

Giả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 ​

quangia_trung_viso_cho_o
Santiago40000.04

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 10% giá cho các chỗ ở nguyên căn như sau:

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.9

Khi 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ị.

  1. Hãy giải thích bản chất kiến trúc bộ nhớ vì sao bảng bao_gia không đổi.
  2. Viết hàm safe_apply_discount(df: pd.DataFrame, room_type: str, giam: float) -> pd.DataFrame khắ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ải

Giả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 ​

indexidprice
010140500.0
110218000.0
210364800.0
3104NaN
41052500.0
510654000.0
610772000.0
710830000.0
810936000.0
911012000.0
10111NaN
1111295000.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ải

Giả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 ​

indexidprice
010140500.0
110218000.0
210364800.0
3104NaN
41052500.0
510654000.0
610772000.0
710830000.0
810936000.0
911012000.0
10111NaN
1111295000.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ácCú pháp chuẩn mựcLợ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àngdf["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àndf.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ảndf.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 ​

  1. 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.
  2. 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/.
  3. 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:

  1. theo_nhan: Giá trị của phần tử mang nhãn nhan, truy xuất thông qua thuộc tính .loc.
  2. 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ải
Giả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ấtGiá trị
theo_nhans_demo.loc[2]100
theo_vi_tris_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ải
Giả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ấtGiá trị
theo_nhans_demo.loc[0]200
theo_vi_tris_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:

  1. thay_doi: Đối tượng pd.Series kế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.
  2. so_nan: Một số nguyên (int) đếm chính xác số lượng giá trị NaN xuất hiện trong Series thay_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ải
Giả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ãnthay_doi
A-10.0
B20.0
CNaN
DNaN

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ải
Giả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ãnthay_doi
A-10.0
B20.0
CNaN
DNaN

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:

  1. Phân khúc rẻ ("re"): Giá phòng nhỏ hơn 30.000 CLP.
  2. Phân khúc trung bình ("trung"): Giá phòng từ 30.000 CLP đến dưới 100.000 CLP.
  3. 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ải
Giả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úcSố dòng trên DEMO
re2
trung5
cao4
Khuyết1

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ải
Giả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úcSố dòng trên DEMO
re2
trung5
cao4
Khuyết1

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 ​

Ảnh & sơ đồ
100%
Ảnh tài liệu được phóng to