Giao diện
Xét bảng dữ liệu đặt phòng khách sạn được xuất từ nhiều chi nhánh khác nhau. Bảng này xuất hiện ô trống ở cột giá, chuỗi văn bản không đồng nhất như ' Entire home ' và 'entire HOME', các bản ghi trùng lặp do gửi hai lần qua mạng, hoặc các mức giá ngoại lai gấp hàng chục lần mức thông thường. Nếu đưa trực tiếp dữ liệu thô này vào các phép toán thống kê hoặc mô hình dự đoán, các kết quả tính toán sẽ bị sai lệch nghiêm trọng.
Tuy nhiên, làm sạch dữ liệu không phải là việc xóa bỏ tùy tiện mọi dòng dữ liệu có vẻ bất thường. Quá trình này đòi hỏi sự cẩn trọng: Nhận diện cơ chế phát sinh dữ liệu thiếu, phân định giữa bản ghi gửi lặp và các sự kiện hợp lệ độc lập, chuẩn hóa văn bản bằng biểu thức chính quy, áp dụng kiểu dữ liệu mở rộng và kiểu phân loại đúng chỗ, đồng thời đo lường độ chính xác của các bước trích xuất bằng các chỉ số thống kê minh bạch. Bài học này làm rõ toàn bộ hệ thống kỹ thuật chuẩn bị dữ liệu trong thư viện pandas.
1. Xử lý giá trị khuyết thiếu (Handling Missing Data)
Trong pandas, giá trị khuyết thiếu được biểu diễn chủ yếu bằng np.nan (đối với dữ liệu số thực), pd.NA (đối với kiểu dữ liệu mở rộng có hỗ trợ giá trị rỗng), hoặc pd.NaT (đối với dữ liệu thời gian).
1.1. Lọc bỏ giá trị khuyết thiếu với dropna
Hàm isna() và notna() là hai công cụ nền tảng trả về mặt nạ boolean để kiểm tra từng phần tử. Khi cần loại bỏ các bản ghi chứa ô trống, phương thức dropna() cung cấp các tham số điều khiển tinh tế:
axis=0(mặc định): Loại bỏ theo hàng, trong khiaxis=1là loại bỏ theo cột.how="any"(mặc định): Xóa nếu hàng hoặc cột chứa ít nhất một ô trống. Trái lại,how="all"chỉ xóa khi toàn bộ các ô trên hàng hoặc cột đó đều trống.thresh=k: Chỉ giữ lại các hàng hoặc cột có ít nhấtgiá trị hợp lệ (khác NA). subset: Chỉ xét sự hiện diện của ô trống trên một danh sách các cột cụ thể, ngăn ngừa việc xóa nhầm các dòng chỉ vì thiếu dữ liệu ở các cột phụ trợ.
python
import numpy as np
import pandas as pd
du_lieu = pd.DataFrame({
"ma_phong": ["P1", "P2", "P3", "P4"],
"gia": [100.0, np.nan, 200.0, np.nan],
"so_phong": [1, 2, np.nan, np.nan],
"ghi_chu": [np.nan, "gần biển", np.nan, np.nan]
})
# Chỉ loại bỏ những hàng thiếu giá trị ở cột 'gia'
df_loc_gia = du_lieu.dropna(subset=["gia"])
print("Bảng sau khi lọc theo cột gia:")
print(df_loc_gia)
assert len(df_loc_gia) == 21.2. Điền giá trị khuyết thiếu với fillna, ffill và bfill
Thay vì xóa bỏ, ta có thể điền giá trị cho các ô trống:
- Điền giá trị cố định: Sử dụng
df.fillna(0)hoặc truyền một từ điển ánh xạ riêng từng cột nhưdf.fillna({"gia": 150, "so_phong": 1}). - Điền theo thứ tự quan sát: Trong pandas 3, tham số
methodtrongfillnađã bị loại bỏ hoàn toàn. Người dùng sử dụng trực tiếp hai phương thức riêng biệt làdf.ffill()(lấy giá trị hợp lệ liền trước điền tới) hoặcdf.bfill()(lấy giá trị hợp lệ liền sau điền lùi lại), hỗ trợ tham sốlimitđể khống chế số bước điền liên tiếp tối đa. - Điền bằng đại lượng thống kê trung tâm: Sử dụng giá trị trung bình (
mean()) hoặc giá trị trung vị (median()) của cột.
1.3. Phân loại nguyên nhân khuyết thiếu và cạm bẫy khi điền giá trị
Trước khi quyết định loại bỏ hay điền giá trị cho các ô trống, nhà phân tích cần xem xét bản chất của dữ liệu thiếu:
- Thiếu ngẫu nhiên do kỹ thuật: Quá trình đo đạc hoặc truyền tải gói tin qua mạng gặp sự cố cục bộ, không phụ thuộc vào bản thân giá trị cần đo. Trong trường hợp này, việc loại bỏ dòng chỉ làm giảm kích thước mẫu quan sát mà không làm lệch ước lượng trung tâm.
- Thiếu có điều kiện theo thuộc tính khác: Xác suất thiếu gắn liền với một đặc tính đã quan sát được (chẳng hạn cơ sở lưu trú ở vùng ngoại ô ít khi cập nhật thông tin tiện nghi hơn cơ sở ở trung tâm thành phố).
- Thiếu gắn liền với giá trị tiềm ẩn: Ô trống xuất hiện chính vì giá trị của nó rơi vào khoảng khó ghi nhận hoặc người cung cấp chủ động bỏ trống trường dữ liệu đó. Việc điền giá trị đơn giản trong tình huống này có thể làm biến dạng bức tranh thống kê thực tế.
Khi điền giá trị thay thế bằng đại lượng thống kê trung tâm (như giá trị trung bình mẫu), nhà phân tích cần chú ý cạm bẫy làm sụt giảm phương sai:
python
s = pd.Series([10.0, 12.0, 14.0, 16.0, 18.0, np.nan])
print("Giá trị trung bình gốc:", s.mean()) # 14.0
print("Phương sai mẫu gốc:", s.var()) # 10.0
# Điền giá trị bằng giá trị trung bình mẫu:
s_dien_tb = s.fillna(s.mean())
print("Giá trị trung bình sau khi điền:", s_dien_tb.mean()) # 14.0
print("Phương sai mẫu sau khi điền:", s_dien_tb.var()) # 8.0Cạm bẫy sụt giảm phương sai nhân tạo
Khi điền giá trị trung bình
2. Biến đổi dữ liệu (Data Transformation)
Biến đổi dữ liệu là bước tái cấu trúc và chuẩn hóa các giá trị trước khi đưa vào mô hình phân tích chuyên sâu.
2.1. Loại bỏ bản ghi trùng lặp: Đơn vị quan sát và xung đột khóa
Phương thức duplicated() trả về mảng boolean đánh dấu các dòng trùng lặp, và drop_duplicates() loại bỏ các dòng này:
- Đơn vị quan sát và bản sao hoàn toàn: Khi hai hàng giống hệt nhau trên mọi cột đang lưu, chỉ có thể xem đó là bản sao lỗi nếu đơn vị quan sát (Unit of Observation) xác nhận mỗi sự kiện kinh tế chỉ được phép xuất hiện duy nhất một lần. Nếu bảng dữ liệu ghi nhận giao dịch mua sắm nhưng không lưu mã đơn hàng hoặc mốc thời gian chi tiết từng giây, hai dòng hoàn toàn giống nhau có thể là hai giao dịch mua hợp lệ và độc lập của cùng một khách hàng. Do đó, việc gọi
drop_duplicates()trên toàn bộ các cột chỉ an toàn khi đã xác định rõ đơn vị quan sát hoặc có xác nhận về lỗi truyền lặp bản ghi. - Xung đột thuộc tính trên khóa (Key Conflicts): Hai hàng có cùng mã định danh
idnhưng các trường thuộc tính khác (như giá hay địa chỉ) lại mang giá trị khác nhau. Tuyệt đối không gọidrop_duplicates(subset=["id"])một cách vội vàng vì thao tác đó sẽ âm thầm loại bỏ một trong hai dữ liệu mâu thuẫn. Cách tiếp cận phù hợp là gắn cờ cảnh báo để đối soát nguồn gốc, hoặc sử dụng mốc thời gian để chỉ giữ lại bản ghi cập nhật sau cùng (keep="last").
python
bang_giao_dich = pd.DataFrame({
"ma_don": ["D1", "D1", "D2", "D2"],
"khach": ["An", "An", "Bình", "Bình"],
"so_tien": [100, 100, 200, 250]
})
# Quét bản sao hoàn toàn trên tất cả các cột
trung_hoan_toan = bang_giao_dich.duplicated()
print("Số dòng trùng hoàn toàn:", int(trung_hoan_toan.sum())) # 1 dòng (D1)
# Phát hiện xung đột khóa trên các dòng không trùng hoàn toàn
bang_khong_trung = bang_giao_dich[~trung_hoan_toan]
xung_dot = bang_khong_trung.duplicated(subset=["ma_don"], keep=False)
print("Các dòng xung đột khóa cần thẩm định:")
print(bang_khong_trung[xung_dot])2.2. Biến đổi bằng hàm hoặc ánh xạ (map)
Phương thức map() trên Series cho phép ánh xạ từng giá trị sang một giá trị mới thông qua một hàm hoặc một từ điển tra cứu:
python
du_lieu_mon = pd.DataFrame({
"ten": ["thịt bò", "cá hồi", "thịt lợn", "thịt bò"],
"khoi_luong": [4.0, 3.0, 12.0, 6.0]
})
anh_xa_nguon = {
"thịt bò": "gia súc",
"cá hồi": "hải sản",
"thịt lợn": "gia súc"
}
du_lieu_mon["nguon_goc"] = du_lieu_mon["ten"].map(anh_xa_nguon)
print(du_lieu_mon)2.3. Thay thế giá trị và đổi tên chỉ mục trục
- Phương thức
replace()cho phép thay thế các giá trị cụ thể bằng giá trị mới, hỗ trợ danh sách hoặc từ điển. - Phương thức
rename(index=..., columns=...)cho phép đổi tên các nhãn hàng và nhãn cột mà không cần gán lại toàn bộ thuộc tính trục:
python
du_lieu_mon = du_lieu_mon.rename(columns={"ten": "ten_thuc_pham", "khoi_luong": "so_kg"})
print(du_lieu_mon.columns.tolist())
assert "ten_thuc_pham" in du_lieu_mon.columns2.4. Rời rạc hóa và chia khoảng (Discretization and Binning)
Để nhóm các biến số thực liên tục thành các khoảng phân loại (ví dụ phân nhóm độ tuổi hoặc mức giá), pandas cung cấp hai hàm:
pd.cut(): Chia theo các điểm cắt cố định do người dùng xác định. Tham sốright=Falsequy định khoảng nửa mở dạng(nhận mút trái , loại mút phải ). pd.qcut(): Chia theo các điểm phân vị mẫu (Quantiles), bảo đảm số lượng quan sát trong mỗi khoảng là xấp xỉ bằng nhau:
python
do_tuoi = pd.Series([20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32])
cac_khoang = [18, 25, 35, 60, 100]
nhom_tuoi = pd.cut(do_tuoi, bins=cac_khoang, right=False)
dem_nhom = nhom_tuoi.value_counts()
print("Phân bố số lượng theo khoảng độ tuổi:")
print(dem_nhom)
assert dem_nhom.loc[pd.Interval(18, 25, closed="left")] == 42.5. Phát hiện và xử lý giá trị ngoại lai bằng Hàng rào Tukey (IQR Fences)
Giá trị ngoại lai (Outlier) là các điểm dữ liệu nằm cách biệt bất thường so với phần còn lại của phân phối. Phương pháp hàng rào Tukey dựa trên Khoảng tứ phân vị (
python
gia_phong = pd.Series([100, 110, 120, 130, 140, 150, 160, 170, 180, 1200])
q1 = gia_phong.quantile(0.25)
q3 = gia_phong.quantile(0.75)
iqr = q3 - q1
lo = q1 - 1.5 * iqr
hi = q3 + 1.5 * iqr
ngoai_lai = (gia_phong < lo) | (gia_phong > hi)
print(f"Khoảng an toàn: [{lo}, {hi}]")
print("Các giá trị ngoại lai:", gia_phong[ngoai_lai].tolist())
assert gia_phong[ngoai_lai].tolist() == [1200]Nguyên tắc sư phạm cốt lõi: Ngoại lai không đồng nghĩa với dữ liệu sai sót. Một căn biệt thự có giá 1.200 USD/đêm là một thực thể kinh doanh hoàn toàn có thật. Nhà phân tích chỉ gắn cờ la_ngoai_lai để tách nhánh phân tích riêng, tuyệt đối không được tự ý xóa bỏ các dòng ngoại lai hợp lệ khỏi tổng thể kinh tế.
2.6. Hoán vị và lấy mẫu ngẫu nhiên
Phương thức sample() trên cho phép lấy mẫu ngẫu nhiên không hoàn lại (replace=False) hoặc có hoàn lại (replace=True), theo số lượng cố định n hoặc theo tỷ lệ frac.
2.7. Tạo biến giả / biến chỉ báo (Dummy / Indicator Variables)
Trong các mô hình thống kê và học máy, các biến phân loại dạng chuỗi cần được chuyển đổi thành các cột nhị phân pd.get_dummies() tự động thực hiện phép biến đổi này:
python
df_loai = pd.DataFrame({"loai": ["nha_rieng", "can_ho", "nha_rieng", "phong_tro"]})
df_dummies = pd.get_dummies(df_loai["loai"], prefix="la", dtype=int)
print(df_dummies)
assert df_dummies.shape == (4, 3)3. Kiểu dữ liệu mở rộng (Extension Data Types)
Trong các phiên bản pandas ban đầu, sự hiện diện của một giá trị khuyết thiếu trong cột số nguyên int64 buộc pandas phải ép kiểu ngầm toàn bộ cột sang số thực float64, vì kiểu số nguyên nguyên thủy của NumPy không có ký hiệu biểu diễn cho NaN.
Để khắc phục hạn chế này, pandas phát triển hệ thống kiểu dữ liệu mở rộng (Extension Dtypes) hỗ trợ khả năng chứa ô trống (Nullable Types):
Int64,Int32,UInt8: Số nguyên cho phép chứapd.NAmà không bị biến đổi sang số thực.boolean: Kiểu logic ba trạng thái (True,False,pd.NA).stringhoặcstring[pyarrow]: Kiểu chuỗi ký tự chuyên dụng, tối ưu hóa bộ nhớ và bảo vệ dữ liệu chuỗi khỏi việc vô tình bị coi là đối tượng Python chung chung (object).
python
# Cột số nguyên có chứa giá trị thiếu nhưng vẫn bảo toàn kiểu nguyên:
s_nguyen = pd.Series([1, 2, None, 4], dtype="Int64")
print(s_nguyen)
assert s_nguyen.dtype == "Int64"
assert pd.isna(s_nguyen.iloc[2])4. Xử lý dữ liệu chuỗi (String Manipulation)
Phần lớn dữ liệu nghiệp vụ và dữ liệu thu thập từ web đều xuất hiện dưới dạng chuỗi văn bản tự do.
4.1. Phương thức chuỗi có sẵn trong Python
Các phương thức tích hợp trên đối tượng str của Python cung cấp khả năng xử lý nhanh từng chuỗi đơn lẻ: Gồm các hàm strip(), split(), join(), find(), replace(), startswith(), endswith().
4.2. Biểu thức chính quy (Regular Expressions)
Mô-đun re của Python cung cấp sức mạnh trích xuất và so khớp mẫu nâng cao:
re.compile: Biên dịch mẫu biểu thức chính quy thành đối tượng mẫu để tái sử dụng nhiều lần.searchvàmatch: Tìm kiếm sự xuất hiện đầu tiên của mẫu trong chuỗi.findall: Tìm toàn bộ các đoạn khớp mẫu.sub: Thay thế các đoạn khớp mẫu bằng chuỗi mới.- Ranh giới từ
\b: Khóa khớp tại ranh giới từ để tránh khớp nhầm chuỗi con nằm bên trong từ khác. - Cờ
re.I(Ignore Case): Khớp không phân biệt chữ hoa và chữ thường.
4.3. Các hàm chuỗi vector hóa trong pandas (.str)
Thuộc tính .str cho phép thực thi các phép biến đổi chuỗi trên toàn bộ một Series mà tự động bỏ qua và bảo toàn các giá trị khuyết thiếu NaN:
- Chuẩn hóa:
.str.strip(),.str.lower(),.str.upper(). - Kiểm tra điều kiện:
.str.contains(),.str.startswith(). - Trích xuất cấu trúc:
.str.extract(),.str.findall().
Cần lưu ý rằng thuộc tính .str là tiện ích duyệt từng phần tử chuỗi Python ở tầng thông dịch. Với các bảng dữ liệu lớn hoặc biểu thức chính quy phức tạp, việc áp dụng liên tiếp nhiều phương thức .str có thể tiêu tốn thời gian hơn so với việc biên dịch trước mẫu regex bằng re.compile() và duyệt xử lý qua hàm map() hoặc list comprehension.
Bẫy ký tự neo regex khi làm sạch chuỗi tiền tệ
Trong biểu thức chính quy, ký tự neo \$ đại diện cho vị trí kết thúc chuỗi văn bản. Nếu gọi s.str.replace(r"\$", "", regex=True), pandas sẽ thay thế vị trí cuối chuỗi bằng chuỗi rỗng và giữ nguyên ký tự đô la ở đầu chuỗi! Để thay thế ký tự tiền tệ thông thường một cách an toàn, luôn chỉ định tham số regex=False: s.str.replace("$", "", regex=False).str.replace(",", "", regex=False).astype(float)
5. Kiểu dữ liệu phân loại (Categorical Data)
Trong các bảng dữ liệu lớn, nhiều cột văn bản chỉ lặp đi lặp lại một số ít các giá trị danh mục cố định (ví dụ tên giới tính, tên thành phố, hoặc mức độ ưu tiên). Việc lưu trữ các chuỗi văn bản này dưới dạng object tiêu tốn bộ nhớ RAM và làm chậm các phép toán gom nhóm so sánh.
5.1. Bản chất và phạm vi hiệu quả của category
Kiểu phân loại category trong pandas sử dụng kỹ thuật mã hóa từ điển (Dictionary Encoding):
categories: Mảng duy nhất chứa các chuỗi danh mục phân biệt.codes: Mảng số nguyên nhỏ (int8hoặcint16) lưu trữ vị trí trỏ tới danh mục của từng hàng.
Khi số lượng danh mục duy nhất nhỏ hơn rất nhiều so với tổng số dòng (tính phân biệt thấp), pandas chỉ lưu trữ một mảng số nguyên nhỏ gọn thay vì lặp lại các chuỗi ký tự, giúp tiết kiệm đáng kể dung lượng bộ nhớ RAM:
python
trai_cay = pd.Series(["táo", "cam", "táo", "táo"] * 2, dtype="category")
print("Các danh mục phân loại:", trai_cay.cat.categories.tolist())
print("Mã số nguyên đại diện:", trai_cay.cat.codes.tolist())
assert trai_cay.cat.codes.tolist() == [1, 0, 1, 1, 1, 0, 1, 1]Tuy nhiên, kiểu category không phải lúc nào cũng mang lại lợi ích về bộ nhớ và tốc độ. Khi một cột có số lượng giá trị phân biệt lớn gần bằng số hàng (chẳng hạn cột mã bưu chính, số điện thoại hoặc văn bản tự do hầu như không lặp lại), việc ép sang category thậm chí còn làm tốn thêm bộ nhớ để lưu mảng từ điển categories và làm chậm tốc độ tính toán do chi phí quản lý bảng mã. Vì vậy, chỉ nên sử dụng category khi tỷ lệ giá trị duy nhất trên tổng số dòng đủ nhỏ.
5.2. Phân loại có thứ tự và các phương thức chuyên dụng
Tham số ordered=True cho phép thiết lập quan hệ thứ tự logic giữa các danh mục (ví dụ: Thứ tự kém < trung bình < khá < giỏi), mở đường cho các phép so sánh toán học và sắp xếp chính xác:
python
danh_gia = pd.Categorical(
["khá", "giỏi", "kém", "trung bình"],
categories=["kém", "trung bình", "khá", "giỏi"],
ordered=True
)
print("Đánh giá lớn nhất:", danh_gia.max())
assert danh_gia.min() == "kém"
assert danh_gia.max() == "giỏi"6. Xác thực văn bản sang bảng và đo lường chất lượng
Trong kỷ nguyên xử lý dữ liệu phi cấu trúc, kỹ sư dữ liệu thường xuyên phải trích xuất các thực thể từ văn bản tự do thành các trường thuộc tính có cấu trúc trong bảng.
6.1. Ràng buộc cấu trúc và kiểm tra định dạng
Một quy trình chuyển đổi văn bản sang bảng đáng tin cậy bắt buộc phải có tầng kiểm soát schema nghiêm ngặt:
- Kiểm tra sự hiện diện của trường bắt buộc: Không chấp nhận các đối tượng thiếu khóa định danh.
- Kiểm tra miền giá trị và kiểu dữ liệu: Sử dụng biểu thức chính quy hoặc logic kiểm tra để đảm bảo dữ liệu số không bị biến tướng thành chuỗi rỗng.
- Phân loại lỗi: Tách riêng các trường hợp trích xuất thất bại để phân tích nguyên nhân thay vì để lỗi làm sập toàn bộ đường ống.
6.2. Đo lường chất lượng trên tập kiểm thử đã gán nhãn
Để đánh giá một phương pháp trích xuất dữ liệu mà không cần phụ thuộc vào bất kỳ dịch vụ hay công cụ trả phí nào, chuẩn mực khoa học là xây dựng một tập kiểm chuẩn nhỏ (khoảng 50 đến 100 mẫu) đã được con người gán nhãn chính xác tuyệt đối (Ground Truth).
Chất lượng trích xuất được đo lường thông qua ba chỉ số toán học:
- Độ chính xác (Accuracy): Tỷ lệ số quan sát được trích xuất hoàn toàn trùng khớp trên tổng số quan sát:
- Độ chuẩn xác (Precision): Trong số những trường hợp hệ thống trích xuất là dương tính, tỷ lệ phần trăm thực sự chính xác là bao nhiêu:
- Độ thu hồi (Recall): Trong số tất cả các thực thể thực sự tồn tại trong văn bản, hệ thống đã trích xuất thành công bao nhiêu phần trăm:
python
# Giả lập kết quả trích xuất thực thể so với nhãn chuẩn:
nhan_thuc_te = [1, 1, 1, 0, 0, 0]
nhan_du_doan = [1, 1, 0, 1, 0, 0]
tp = sum(t == 1 and p == 1 for t, p in zip(nhan_thuc_te, nhan_du_doan))
fp = sum(t == 0 and p == 1 for t, p in zip(nhan_thuc_te, nhan_du_doan))
fn = sum(t == 1 and p == 0 for t, p in zip(nhan_thuc_te, nhan_du_doan))
tn = sum(t == 0 and p == 0 for t, p in zip(nhan_thuc_te, nhan_du_doan))
do_chinh_xac = (tp + tn) / len(nhan_thuc_te)
do_chuan_xac = tp / (tp + fp)
do_thu_hoi = tp / (tp + fn)
print(f"Accuracy: {do_chinh_xac:.2f}, Precision: {do_chuan_xac:.2f}, Recall: {do_thu_hoi:.2f}")
assert np.isclose(do_chinh_xac, 4/6)
assert np.isclose(do_chuan_xac, 2/3)
assert np.isclose(do_thu_hoi, 2/3)8. Tổng kết
- Thận trọng với cơ chế khuyết thiếu: Phân biệt rõ các tình huống thiếu ngẫu nhiên do kỹ thuật, thiếu có điều kiện hay thiếu gắn liền với giá trị tiềm ẩn. Tránh điền giá trị trung bình một cách máy móc làm sụt giảm phương sai và thu hẹp khoảng tin cậy.
- Phân định rõ ranh giới trùng lặp: Xác lập đơn vị quan sát trước khi xóa các hàng trùng hoàn toàn. Đối với xung đột thuộc tính trên cùng một khóa chính, không dùng
drop_duplicatesđể che đậy các mâu thuẫn nghiệp vụ mà cần gắn cờ cảnh báo hoặc lưu bản ghi sau cùng. - Khai thác hàng rào Tukey IQR: Nhận diện ngoại lai dựa trên khoảng tứ phân vị. Chỉ gắn cờ phân loại để phân tích chuyên sâu chứ không tự ý xóa bỏ các thực thể kinh tế hợp lệ.
- Làm chủ biểu thức chính quy và hàm chuỗi: Tận dụng thuộc tính
.strđể biến đổi vector hóa, đồng thời luôn chỉ địnhregex=Falsekhi thay thế các ký tự văn bản thông thường như dấu đô la$. - Cân nhắc phạm vi của kiểu phân loại Categorical: Tận dụng cơ chế mã hóa từ điển của kiểu
categorycho các cột văn bản có số lượng nhãn phân biệt nhỏ so với quy mô dòng để tối ưu hóa bộ nhớ. - Kiểm soát chất lượng bằng thước đo toán học: Đánh giá các quy trình trích xuất văn bản thành bảng bằng các chỉ số khách quan như Accuracy, Precision và Recall trên tập mẫu kiểm thử đã gán nhãn chuẩn mực.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 7: Data Cleaning and Preparation.
Tài liệu chính thức pandas: Working with missing data & Categorical data.
John W. Tukey, Exploratory Data Analysis, Addison-Wesley, 1977.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
RegexOne, các chủ đề luyện biểu thức chính quy. Bộ đề và ca thử IAI.
7. Bài tập rèn luyện
Bài tập 1: Giá trung vị theo loại chỗ ở
Một bảng dữ liệu lưu trữ loại chỗ ở và giá thuê (USD/đêm) gặp tình trạng ô trống và định dạng chuỗi không thống nhất như sau:
| Loại | Giá |
|---|---|
| " Entire home " | 300 |
| "entire HOME" | thiếu |
| "PRIVATE ROOM" | 40 |
| "Private room " | 60 |
| "private room" | 200 |
Hãy viết mã nguồn tính giá trung vị theo từng loại chỗ ở, trong đó các nhãn chỉ khác nhau về chữ hoa chữ thường hoặc khoảng trắng thừa ở hai đầu phải được gộp chung vào cùng một nhóm.
Lời giải
Ta sử dụng phương thức chuỗi vector hóa .str.strip().str.lower() để chuẩn hóa cột loại chỗ ở về dạng chữ thường không có khoảng trắng thừa, sau đó gom nhóm và tính giá trị trung vị (median()):
python
bang_cho_o = pd.DataFrame({
"loai": [" Entire home ", "entire HOME", "PRIVATE ROOM", "Private room ", "private room"],
"gia": [300.0, np.nan, 40.0, 60.0, 200.0]
})
loai_chuan = bang_cho_o["loai"].str.strip().str.lower()
gia_trung_vi = bang_cho_o.groupby(loai_chuan)["gia"].median()
print("Giá trung vị theo loại chỗ ở sau chuẩn hóa:")
print(gia_trung_vi)
assert gia_trung_vi["entire home"] == 300.0
assert gia_trung_vi["private room"] == 60.0Nhóm entire home chỉ có một quan sát có giá là 300 (dòng thứ hai bị thiếu giá), do đó giá trung vị là 300. Nhóm private room có ba quan sát với các giá trị 40, 60 và 200. Giá trị trung vị của ba số này là 60.
Bài tập 2: Trích xuất diện tích phòng bằng biểu thức chính quy
Cho cột tên phòng mô tả tự do của các cơ sở lưu trú như sau:
| Tên phòng | Diện tích kỳ vọng ( |
|---|---|
| Depto 45 m2 | 45 |
| Studio 30m² | 30 |
| 80 M2 luxury | 80 |
| Torre 2024 | thiếu |
| 30m2x | thiếu |
Hãy thiết kế một biểu thức chính quy duy nhất để trích xuất chính xác diện tích phòng thành giá trị số nguyên. Yêu cầu: Không trích xuất các năm như 2024 và không khớp nhầm các từ có chứa hậu tố bất thường như 30m2x.
Lời giải
Biểu thức chính quy cần thỏa mãn:
- Bắt đầu bằng ranh giới từ
\b. - Khớp nhóm các chữ số diện tích
(\d+). - Khớp khoảng trắng tùy chọn
\s*đi kèm đơn vịm2hoặcm²: Mẫum(?:2|²). - Kết thúc bằng ranh giới từ
\bđể loại trừ trường hợp30m2x. - Bổ sung cờ
re.Iđể nhận diện cả chữ hoaM2.
python
import re
ten_phong = pd.Series(["Depto 45 m2", "Studio 30m²", "80 M2 luxury", "Torre 2024", "30m2x"])
# Thiết kế mẫu regex và trích xuất
mau_regex = r"\b(\d+)\s*m(?:2|²)\b"
dien_tich_chuoi = ten_phong.str.extract(mau_regex, flags=re.I, expand=False)
dien_tich_so = pd.to_numeric(dien_tich_chuoi)
print("Kết quả trích xuất diện tích:")
print(dien_tich_so)
assert dien_tich_so.iloc[0] == 45.0
assert dien_tich_so.iloc[1] == 30.0
assert dien_tich_so.iloc[2] == 80.0
assert pd.isna(dien_tich_so.iloc[3])
assert pd.isna(dien_tich_so.iloc[4])Bài tập 3: Thống kê tần số tiện nghi độc lập
Một bảng dữ liệu lưu trữ danh sách các tiện nghi của từng chỗ ở dưới dạng danh sách chuỗi:
| Mã chỗ ở | Danh sách tiện nghi |
|---|---|
| P1 | ["Wifi", " wifi ", "Kitchen"] |
| P2 | ["WIFI", "Kitchen"] |
| P3 | ["Kitchen", "Parking", "Parking"] |
| P4 | [] |
Hãy viết mã nguồn thống kê xem mỗi tiện nghi được bao nhiêu chỗ ở độc lập liệt kê. Yêu cầu: Gộp các tiện nghi chỉ khác nhau về chữ hoa chữ thường hoặc khoảng trắng thừa ở hai đầu, và mỗi tiện nghi xuất hiện nhiều lần trong cùng một chỗ ở chỉ được tính là một lần.
Lời giải
Quy trình xử lý gồm ba bước:
- Sử dụng phương thức
explode()để bung từng phần tử danh sách thành các hàng riêng biệt và loại bỏ các hàng rỗng. - Chuẩn hóa chuỗi bằng
.str.strip().str.lower(). - Sử dụng
drop_duplicates(subset=["cho_o", "tien_nghi"])để đảm bảo mỗi tiện nghi chỉ được đếm một lần cho mỗi chỗ ở, sau đó dùngvalue_counts():
python
df_tien_nghi = pd.DataFrame({
"cho_o": ["P1", "P2", "P3", "P4"],
"danh_sach": [
["Wifi", " wifi ", "Kitchen"],
["WIFI", "Kitchen"],
["Kitchen", "Parking", "Parking"],
[]
]
})
# Bung danh sách thành các hàng
df_bung = df_tien_nghi.explode("danh_sach").dropna()
df_bung["danh_sach"] = df_bung["danh_sach"].str.strip().str.lower()
# Loại trùng lặp trong cùng một cơ sở lưu trú
df_duy_nhat = df_bung.drop_duplicates(subset=["cho_o", "danh_sach"])
# Đếm số lượng cơ sở lưu trú có từng tiện nghi
thong_ke_tn = df_duy_nhat["danh_sach"].value_counts()
print(thong_ke_tn)
assert thong_ke_tn["kitchen"] == 3
assert thong_ke_tn["wifi"] == 2
assert thong_ke_tn["parking"] == 1Kết quả: Tiện nghi kitchen có mặt tại 3 chỗ ở (P1, P2, P3), tiện nghi wifi có mặt tại 2 chỗ ở (P1, P2), và tiện nghi parking có mặt tại 1 chỗ ở (P3).
Bài tập 4: So sánh giá giữa hai kỳ quan sát
Bảng dữ liệu theo dõi giá thuê phòng (USD/đêm) của cùng ba chỗ ở qua hai kỳ tháng 5 (T5) và tháng 6 (T6), trong đó chỗ ở P3 bị thiếu giá ở kỳ T6:
python
bang_gia_ky = pd.DataFrame({
"T5": [100.0, 200.0, 300.0],
"T6": [110.0, 220.0, np.nan]
}, index=["P1", "P2", "P3"])Nếu tính trung bình gộp từng cột: Kỳ T5 có giá trung bình là 200, trong khi kỳ T6 có giá trung bình là 165 (giảm 35 USD). Hãy phân tích cạm bẫy của phép so sánh này và viết mã nguồn so sánh lại giá trung bình một cách khoa học.
Lời giải
Phép tính trung bình gộp bị sai lệch nghiêm trọng do thành phần mẫu thay đổi: Kỳ T5 tính trên cả 3 chỗ ở, bao gồm căn hộ cao cấp P3 (giá 300). Sang kỳ T6, căn hộ đắt nhất bị thiếu dữ liệu, khiến mẫu chỉ còn lại hai căn hộ bình dân P1 và P2. Con số giảm từ 200 xuống 165 phản ánh việc mất mẫu của căn hộ đắt tiền chứ không phản ánh xu hướng giá thị trường.
Để so sánh công bằng (Paired Comparison), ta chỉ so sánh trên những chỗ ở có đầy đủ dữ liệu ở cả hai kỳ bằng cách gọi dropna() trước khi tính trung bình:
python
bang_gia_ky = pd.DataFrame({
"T5": [100.0, 200.0, 300.0],
"T6": [110.0, 220.0, np.nan]
}, index=["P1", "P2", "P3"])
# So sánh trên tập quan sát có dữ liệu ở cả hai kỳ
bang_cap_doi = bang_gia_ky.dropna()
tb_chuan = bang_cap_doi.mean()
print("Giá trung bình cặp đôi chuẩn mực:")
print(tb_chuan)
assert tb_chuan["T5"] == 150.0
assert tb_chuan["T6"] == 165.0Khi xét trên cùng hai chỗ ở P1 và P2, giá trung bình thực tế tăng từ 150 lên 165 (tăng
Bài tập 5: Phản xạ của bộ kiểm tra ngoại lai trước đợt tăng giá đồng loạt
Một hệ thống kiểm toán dữ liệu tự động định kỳ tính ngưỡng bách phân vị
Giả sử ở kỳ tiếp theo, danh sách các cơ sở lưu trú được giữ nguyên vẹn nhưng mọi mức giá trên toàn thị trường đều tăng gấp đôi một cách đồng loạt do lạm phát. Hỏi: Dựa vào tỷ lệ bản ghi bị gắn cờ cảnh báo, bộ kiểm tra này có phát hiện ra đợt tăng giá đột biến trên toàn thị trường hay không? Giải thích bản chất toán học.
Lời giải
Bộ kiểm tra này hoàn toàn không phát hiện được đợt tăng giá đồng loạt.
Về bản chất toán học: Phép biến đổi nhân toàn bộ tập dữ liệu với một hằng số dương
Gọi
Mặt nạ boolean gắn cờ và tỷ lệ các bản ghi bị đánh dấu ở kỳ mới hoàn toàn trùng khớp với kỳ cũ. Ngưỡng phân vị tương đối chỉ phát hiện các điểm dị biệt so với nội bộ phân phối của chính kỳ đó chứ không có khả năng phát hiện sự dịch chuyển mức độ của toàn bộ hệ thống qua các mốc thời gian.
Bài tập 6: Giữ giá cập nhật sau cùng theo từng khóa
Một bảng dữ liệu ghi nhận lịch sử các lần cập nhật giá của các phòng như sau:
| id | ky | cap_nhat | gia |
|---|---|---|---|
| P1 | T6 | 2026-06-29 10:00 | 120 |
| P2 | T6 | 2026-06-29 08:00 | 90 |
| P1 | T5 | 2026-05-31 07:00 | 100 |
| P1 | T6 | 2026-06-29 09:00 | 110 |
Với mỗi cặp mã chỗ ở và kỳ quan sát (id, ky), hãy viết mã nguồn chọn ra mức giá được cập nhật sau cùng, bất kể các dòng dữ liệu ban đầu nằm ở thứ tự nào trong bảng.
Lời giải
Ta sắp xếp bảng dữ liệu theo thứ tự thời gian tăng dần của cột cap_nhat, sau đó áp dụng phương thức drop_duplicates() trên tập khóa ["id", "ky"] và chỉ định tham số keep="last" để giữ lại dòng cuối cùng:
python
lich_cap_nhat = pd.DataFrame({
"id": ["P1", "P2", "P1", "P1"],
"ky": ["T6", "T6", "T5", "T6"],
"cap_nhat": pd.to_datetime(["2026-06-29 10:00", "2026-06-29 08:00", "2026-05-31 07:00", "2026-06-29 09:00"]),
"gia": [120, 90, 100, 110]
})
# Sắp xếp theo mốc thời gian và giữ lại bản ghi sau cùng
gia_sau_cung = (
lich_cap_nhat
.sort_values("cap_nhat")
.drop_duplicates(subset=["id", "ky"], keep="last")
)
print("Các mức giá cập nhật sau cùng:")
print(gia_sau_cung)
ket_qua_kiem_tra = set(zip(gia_sau_cung["id"], gia_sau_cung["ky"], gia_sau_cung["gia"]))
assert ket_qua_kiem_tra == {("P1", "T6", 120), ("P2", "T6", 90), ("P1", "T5", 100)}Với cặp (P1, T6), bản ghi lúc 10:00 với mức giá 120 được giữ lại, bản ghi lúc 09:00 với mức giá 110 bị loại bỏ chính xác.
Lab 7: Chuỗi, regex và danh sách tiện nghi
Một cột văn bản có thể chứa khoảng trắng thừa, thẻ xuống dòng, giá viết dưới dạng chuỗi hoặc danh sách JSON. Ta sẽ làm sạch từng dạng, kiểm lại những chuỗi không chuyển đổi được và trích thông tin theo quy tắc rõ ràng. Các ví dụ cũng cho thấy vì sao có một từ khóa chưa đủ để suy ra ý kiến của người viết.
Thông tin dữ liệu thực hành
Tập bài tập sử dụng dữ liệu chỗ ở và đánh giá của Inside Airbnb với ảnh chụp dữ liệu (snapshot) ngày 29/06/2026 tại thành phố Santiago (Chile). Tệp dữ liệu đánh giá đầy đủ được lưu trữ tại: https://data.insideairbnb.com/chile/rm/santiago/2026-06-29/data/reviews.csv.gz (gồm 690.112 đoạn văn bản trong cột comments). Tệp dữ liệu chỗ ở đầy đủ được lưu trữ tại: https://data.insideairbnb.com/chile/rm/santiago/2026-06-29/data/listings.csv.gz (gồm 18.534 chỗ ở trong cột amenities).
Lưu ý: Các số liệu tính toán trên các tập dữ liệu nhỏ dưới đây là dữ liệu kiểm thử giả lập (synthetic fixtures), phục vụ mục đích kiểm định tính đúng đắn của giải thuật và không đại diện cho các chỉ số thống kê toàn cục của thị trường Santiago. Biểu thức chính quy chỉ đóng vai trò là một quy tắc mẫu có phạm vi giới hạn, không thay thế cho các bộ nhận diện ngôn ngữ chuyên sâu. Thao tác thay thế chuỗi <br/> trong bài chỉ áp dụng cho đúng thẻ ngắt dòng nguyên văn đã biết, không dùng để phân tích cú pháp HTML tổng quát.
python
import json
import re
import numpy as np
import pandas as pd
from IPython.display import display
# Dữ liệu kiểm thử giả lập 16 đoạn đánh giá đại diện
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
# Dữ liệu kiểm thử chuyển đổi giá tiền
DEMO_PRICES = pd.Series(["$1,200.00", " $850.00 ", None, "liên hệ", "từ $500"])
DEMO_PRICES_TEST = pd.Series(
["$2,450.00", " $95 ", "$24,50.00", "khoảng $300.00", "$95.5", None]
)
# Dữ liệu kiểm thử danh sách tiện nghi JSON
DEMO_LISTINGS = pd.DataFrame(
{
"amenities": [
'["Wifi", "Air conditioning"]',
'["Wifi, free", "Kitchen"]',
"[]",
'["Kitchen"]',
'["Heating"]',
]
},
index=[101, 102, 103, 104, 105],
)
# Dữ liệu kiểm thử phân biệt danh sách rỗng và giá trị khuyết thiếu
DEMO_DS_NHO = pd.Series(
[
["Wifi", "Wifi", "Kitchen"],
["Kitchen"],
[],
None,
["Wifi"],
],
index=["P1", "P2", "P3", "P4", "P5"],
)
PAT_ES = r"ción|ñ|muy|excelente|departamento"
PAT_GAN = r"(?:cerca de la |cerca del |cerca de |near the |near )([A-Za-zÁ-Úá-úñÑ]+)"
PAT_GIA = r"\$(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{2})?"Danh mục bài tập thực hành
Bài 1: Chuẩn hoá chuỗi văn bản trước khi đếm tần suất
Thực hiện thao tác chuẩn hoá định dạng cơ bản cho cột chuỗi ký tự bằng cách loại bỏ khoảng trắng dư thừa ở hai đầu và chuyển toàn bộ về chữ thường. Không tự ý loại bỏ dấu câu nếu quy tắc chưa yêu cầu.
Yêu cầu kỹ thuật:
- Tình huống: Chuỗi văn bản nhập liệu thường xuất hiện khoảng trắng thừa hoặc viết hoa không đồng nhất như
" Wifi ","wifi","WIFI!","wi-fi". - Thao tác: Dùng phương thức
.str.strip()kết hợp.str.lower(). - Đầu ra: Trả về Series mới đã được chuẩn hoá, giữ nguyên và bảo toàn giá trị khuyết thiếu.
- Trường hợp biên: Dữ liệu chứa phần tử
Nonehoặcnp.nanphải tiếp tục giữ nguyên trạng thái khuyết thiếu sau khi xử lý.
Lời giải
Cách 1: Chuẩn hóa trực tiếp một Series
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
s = pd.Series([" Wifi ", "wifi", "WIFI!", "wi-fi"])
s_chuan = s.str.strip().str.lower()
print("Kết quả chuẩn hoá Cách 1:")
print(s_chuan.tolist())
print(f"Số lượng phần tử bằng đúng wifi: {(s_chuan == 'wifi').sum()}")
# Kết thúc lời giảiGiải thích Cách 1
.str cho phép gọi cùng một phép xử lý chuỗi trên từng ô của Series. .strip() bỏ khoảng trắng, dấu tab hoặc ký tự xuống dòng ở hai đầu, rồi .lower() đổi chữ hoa thành chữ thường. Chuỗi phương thức đọc từ trái sang phải theo thứ tự biến đổi.
Dấu chấm than trong WIFI! nằm trong nội dung cần giữ, nên kết quả là wifi!. Dấu nối trong wi-fi cũng không bị bỏ. Do đó (s_chuan == "wifi").sum() chỉ đếm hai dòng bằng đúng wifi, không phải mọi cách viết có thể chỉ cùng tiện nghi.
Các phương thức tạo kết quả mới và giữ . Ô khuyết vẫn ở trạng thái khuyết, còn chuỗi rỗng là một chuỗi có thật, khác ô thiếu dữ liệu. Quy tắc này chỉ chuẩn hóa khoảng trắng ngoài và kiểu chữ, chưa xử lý dấu câu hoặc đồng nhất từ đồng nghĩa.
Output Cách 1
| Vị trí | Đầu vào | Kết quả |
|---|---|---|
| 0 | " Wifi " | wifi |
| 1 | wifi | wifi |
| 2 | WIFI! | wifi! |
| 3 | wi-fi | wi-fi |
Lệnh đếm bằng đúng wifi trả 2.
Cách 2: Đưa quy tắc chuẩn hóa vào một hàm dùng lại
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
def normalize(s: pd.Series) -> pd.Series:
"""Loại bỏ khoảng trắng hai đầu và chuyển thành chữ thường.
Tham số s: Series chuỗi nguồn cần chuẩn hoá.
Trả về: Series chuỗi mới cùng chỉ mục, bảo toàn giá trị khuyết thiếu.
"""
return s.str.strip().str.lower()
mau_chuoi = pd.Series([" Wifi ", "wifi", "WIFI!", "wi-fi"])
ket_qua_q1 = normalize(mau_chuoi)
print("Kết quả từ hàm normalize:")
print(ket_qua_q1.tolist())
print(
f"Kiểm tra bảo toàn giá trị khuyết thiếu: {normalize(pd.Series([' A ', None])).isna().tolist()}"
)
# Kết thúc lời giảiGiải thích Cách 2
normalize(s) nhận Series đầu vào và trả ngay kết quả .str.strip().str.lower(). Hàm không nhận tên cột, nên người gọi có thể truyền bất kỳ cột chuỗi nào mà vẫn dùng cùng quy tắc. Việc đặt logic trong hàm giúp sửa hoặc kiểm thử một nơi thay vì chép hai phép xử lý ở nhiều chỗ.
Phần kiểm tra normalize(pd.Series([" A ", None])).isna() tạo hai cờ thiếu dữ liệu. Chuỗi đầu thành a, nên cờ đầu là False. Ô thứ hai vẫn thiếu, nên cờ sau là True. Kết quả [False, True] kiểm tra trạng thái khuyết, không phải hai chuỗi trả về.
.lower() không phải quy tắc đồng nhất mọi cách viết của Unicode hoặc mọi tên tiện nghi. Nếu cần ghép tên từ nhiều ngôn ngữ, có thể phải thống nhất thêm quy tắc mã hóa hoặc dùng casefold, nhưng phải kiểm tra tác động trước khi đổi yêu cầu của bài.
Output Cách 2
| Vị trí | Đầu vào | Kết quả |
|---|---|---|
| 0 | " Wifi " | wifi |
| 1 | wifi | wifi |
| 2 | WIFI! | wifi! |
| 3 | wi-fi | wi-fi |
Kiểm tra ô khuyết in [False, True].
So sánh hai cách
Hai cách dùng cùng hai thao tác chuỗi nên cho cùng giá trị. Cách 1 dễ quan sát trên một cột. Cách 2 giúp dùng lại và kiểm thử thống nhất, chứ không tự làm phép xử lý chạy nhanh hơn. Chỉ bổ sung quy tắc loại dấu câu nếu mục tiêu dữ liệu thật sự yêu cầu.
Bài 2: Bẫy giá trị thiếu và ký tự đặc biệt trong so khớp chuỗi
Phân tích hiện tượng lỗi logic khi kiểm tra sự tồn tại của chuỗi con trên cột có chứa giá trị khuyết thiếu hoặc chứa ký tự đặc biệt của biểu thức chính quy.
Yêu cầu kỹ thuật:
- Tình huống: Cách xử lý ô thiếu của
s.str.contains("centro")mặc định phụ thuộc kiểu chuỗi và phiên bản pandas. Cần đặtna=Falseđể mọi dòng thiếu nhận cờ không khớp rõ ràng khi đếm hoặc lọc. Ngoài ra, chuỗi tìm kiếm như"50.000"nếu không chỉ địnhregex=Falsesẽ coi dấu chấm là ký tự đại diện cho mọi ký tự. - Thao tác: Thiết lập rõ ràng hai tham số
regex=Falsevàna=Falsetrong phương thức.str.contains(). - Đầu ra: Trả về số lượng dòng nguyên vẹn chứa đúng chuỗi con chỉ định dưới dạng số nguyên int.
- Trường hợp biên: Tìm kiếm phân biệt hoa thường và chuỗi chứa dấu chấm số thập phân.
Lời giải
Cách 1: Tạo cờ chứa chuỗi nguyên văn rồi cộng số cờ đúng
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
t = pd.Series(["depto centro", None, "casa con vista"])
so_dong_co_centro = int(t.str.contains("centro", regex=False, na=False).sum())
print(f"Số dòng chứa từ centro: {so_dong_co_centro}")
# Kết thúc lời giảiGiải thích Cách 1
str.contains("centro", regex=False, na=False) hỏi mỗi dòng có chứa đúng chuỗi con centro hay không. regex=False tắt cách hiểu ký tự đặc biệt của regex, còn na=False coi dòng thiếu dữ liệu là không khớp. Kết quả là mặt nạ rõ trạng thái đúng sai cho từng dòng.
.sum() cộng các cờ đúng để đếm số dòng khớp. Một dòng có từ lặp nhiều lần vẫn chỉ đóng góp 1, vì contains trả một cờ cho cả dòng. int(...) đưa số đếm về kiểu số nguyên Python. Phép tìm kiếm vẫn phân biệt hoa thường vì không truyền case=False.
Không nên cho rằng mọi cách gọi contains mặc định đều làm .sum() lỗi. Cách biểu diễn giá trị khuyết phụ thuộc kiểu chuỗi và phiên bản pandas, và phép cộng thường bỏ qua ô thiếu. Đặt na=False ở đây giúp quy tắc mặt nạ nhất quán và dễ dùng trong các bước lọc tiếp theo.
Output Cách 1
| Dòng | Nội dung | Khớp centro |
|---|---|---|
| 0 | depto centro | True |
| 1 | Khuyết | False |
| 2 | casa con vista | False |
Số dòng chứa centro là 1.
Cách 2: Viết hàm đếm với chuỗi tìm kiếm làm tham số
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
def count_contains(s: pd.Series, tu: str) -> int:
"""Đếm số dòng chứa chuỗi con theo so khớp nguyên văn an toàn.
Tham số s: Series chuỗi nguồn cần kiểm tra.
Tham số tu: Chuỗi con cần tìm kiếm nguyên văn.
Trả về: Số nguyên biểu diễn số dòng thoả mãn điều kiện.
"""
return int(s.str.contains(tu, regex=False, na=False).sum())
vi_du_t = pd.Series(["depto centro", None, "casa con vista"])
ket_qua_q2 = count_contains(vi_du_t, "centro")
print(f"Đếm từ centro trong vi_du_t: {ket_qua_q2}")
vi_du_gia = pd.Series(["giá 50.000", "giá 50,000"])
print(f"Đếm chuỗi 50.000 nguyên văn: {count_contains(vi_du_gia, '50.000')}")
# Kết thúc lời giảiGiải thích Cách 2
count_contains(s, tu) tách dữ liệu và từ khóa khỏi quy tắc so khớp. Nhờ regex=False, từ khóa như 50.000 yêu cầu đúng dấu chấm. Nếu bật regex, dấu . có nghĩa một ký tự bất kỳ và có thể khớp cả 50,000.
na=False cho phép cộng cờ hoặc dùng mặt nạ mà không cần thay ô thiếu bằng một chuỗi giả trước. Việc thay thiếu thành chuỗi rỗng có thể hữu ích ở bài khác, nhưng sẽ thay đổi giá trị dữ liệu và cần được cân nhắc riêng.
Hai lần gọi minh họa dùng hai bộ dữ liệu khác nhau. Lần đầu tìm centro và có một dòng khớp. Lần sau tìm 50.000 trong hai chuỗi giá, chỉ chuỗi có dấu chấm được tính. So khớp nguyên văn dựa vào ký tự của chuỗi, không phải thao tác so byte hay một phép chuẩn hóa tiền tệ.
Output Cách 2
| Lời gọi | Số dòng khớp |
|---|---|
count_contains(vi_du_t, "centro") | 1 |
count_contains(vi_du_gia, "50.000") | 1 |
So sánh hai cách
Cách 1 dùng trực tiếp trên một tình huống. Cách 2 cho phép dùng lại với nhiều từ khóa mà vẫn giữ quy tắc khuyết và dấu câu. Cả hai đang đếm dòng chứa chuỗi con. Nếu cần đếm số lần xuất hiện trong mỗi dòng hoặc cần khớp cả từ, phải chọn phép xử lý và mẫu khác.
Bài 3: Chuyển đổi định dạng số tiền và phát hiện lỗi đọc dữ liệu
Thực hiện chuyển đổi cột dữ liệu giá tiền từ định dạng chuỗi ký tự sang số thực, đồng thời gắn cờ nhận diện các dòng có dữ liệu nhưng sai quy cách chuyển đổi.
Yêu cầu kỹ thuật:
- Tình huống: Giá tiền dạng chuỗi có thể chứa ký hiệu
$, dấu phẩy phân cách hàng nghìn, khoảng trắng, hoặc văn bản tự do như"liên hệ","từ $500". - Thao tác: Loại bỏ ký hiệu
$và dấu phẩy,, dùngpd.to_numeric(..., errors='coerce')để chuyển đổi sang số thực. Sau đó so sánh điều kiện dữ liệu ban đầu không khuyết thiếu nhưng số thực nhận giá trịNaNđể đánh dấu cờ lỗi đọc. - Đầu ra: Trả về gồm ba cột:
gia(chuỗi gốc),gia_so(số thực sau chuyển đổi) vàloi_doc(giá trị boolean đánh dấu lỗi). - Trường hợp biên: Giá trị
Noneban đầu là thiếu tự nhiên, không tính là lỗi đọc (loi_doc = False).
Lời giải
Cách 1: Làm sạch chuỗi rồi giữ cờ chuyển đổi thất bại
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
# Bắt đầu lời giải
gia_raw = pd.Series(["$1,200.00", " $850.00 ", None, "liên hệ", "từ $500"])
gia_clean = (
gia_raw.str.strip()
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
)
gia_num = pd.to_numeric(gia_clean, errors="coerce")
loi = gia_raw.notna() & gia_num.isna()
df_gia_cach1 = pd.DataFrame({"gia": gia_raw, "gia_so": gia_num, "loi_doc": loi})
display(df_gia_cach1)
# Kết thúc lời giảiGiải thích Cách 1
.str.strip() bỏ khoảng trắng ở hai đầu, rồi hai lần .replace(..., regex=False) xóa ký hiệu đô la và dấu phẩy. pd.to_numeric(..., errors="coerce") thử đọc chuỗi còn lại như một số. Các chữ liên hệ và tiền tố từ không bị xóa, nên hai dòng đó không chuyển được.
gia_raw.notna() đánh dấu nơi nguồn thật sự có dữ liệu. gia_num.isna() đánh dấu nơi kết quả số đang khuyết. Phép & chỉ giữ cờ đúng nếu đồng thời có dữ liệu gốc và chuyển đổi thất bại. Vì vậy ô None không bị báo lỗi đọc.
pd.DataFrame({...}) ghép ba Series theo cùng để giữ chuỗi gốc cạnh số và cờ lỗi. display trong notebook hiển thị bảng với tiêu đề cột và từng hàng, giúp nhìn đúng dòng phải rà lại thay vì chỉ biết có hai ô khuyết mới.
Output Cách 1
| Vị trí | gia | gia_so | loi_doc |
|---|---|---|---|
| 0 | $1,200.00 | 1200.0 | False |
| 1 | " $850.00 " | 850.0 | False |
| 2 | Khuyết | NaN | False |
| 3 | liên hệ | NaN | True |
| 4 | từ $500 | NaN | True |
Cách 2: Đóng gói chuyển đổi và chẩn đoán trong cùng hàm
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
DEMO_PRICES = pd.Series(["$1,200.00", " $850.00 ", None, "liên hệ", "từ $500"])
# Bắt đầu lời giải
def parse_price(gia: pd.Series) -> pd.DataFrame:
"""Chuyển đổi chuỗi giá tiền sang số thực và gắn cờ các dòng lỗi định dạng.
Tham số gia: Series chuỗi giá tiền cần xử lý.
Trả về: DataFrame gồm cột gia gốc, gia_so sau chuyển đổi và cờ loi_doc.
"""
gia_sach = (
gia.str.strip()
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
)
gia_so = pd.to_numeric(gia_sach, errors="coerce")
loi_doc = gia.notna() & gia_so.isna()
return pd.DataFrame({"gia": gia, "gia_so": gia_so, "loi_doc": loi_doc})
df_ket_qua_gia = parse_price(DEMO_PRICES)
display(df_ket_qua_gia)
# Kết thúc lời giảiGiải thích Cách 2
parse_price(gia) nhận một Series giá và tạo hai Series mới là gia_so và loi_doc. Kết quả giữ cột gia gốc để người học truy lại nguyên nhân của từng cờ lỗi. được pandas căn chỉnh khi dựng bảng, nên các Series phải cùng xuất phát từ đầu vào đó.
errors="coerce" không xác thực quy cách tiền tệ trước khi đọc số. Chẳng hạn xóa dấu phẩy trong một chuỗi viết sai dấu nhóm vẫn có thể tạo một số đọc được. Bài tiếp theo dùng fullmatch để kiểm tra toàn bộ cấu trúc, đây là mục tiêu khác với chỉ thử chuyển sang số.
Hàm không tự lấy 500 từ chuỗi từ $500, vì điều đó sẽ bỏ mất ý nghĩa giá bắt đầu từ một ngưỡng. Giữ lỗi để kiểm tra phù hợp hơn việc âm thầm diễn giải lại quy tắc dữ liệu.
Output Cách 2
| Vị trí | gia | gia_so | loi_doc |
|---|---|---|---|
| 0 | $1,200.00 | 1200.0 | False |
| 1 | " $850.00 " | 850.0 | False |
| 2 | Khuyết | NaN | False |
| 3 | liên hệ | NaN | True |
| 4 | từ $500 | NaN | True |
So sánh hai cách
Cách 1 giúp theo dõi từng Series trung gian. Cách 2 giữ một giao diện đầu vào và một bảng chẩn đoán dùng lại được. Hai cách có cùng thuật toán, chưa có khác biệt hiệu năng được đo. Điều cốt lõi là tách dữ liệu vốn thiếu với dữ liệu hiện diện nhưng không đọc được.
Bài 4: Xác thực toàn bộ chuỗi định dạng tiền tệ bằng biểu thức chính quy
Kiểm tra tính hợp lệ toàn diện của chuỗi biểu diễn số tiền bằng cách đối chiếu toàn bộ cấu trúc với mẫu biểu thức chính quy quy định, tránh việc chỉ kiểm tra một phần chuỗi gây lọt dữ liệu sai.
Yêu cầu kỹ thuật:
- Tình huống: Một chuỗi như
"$24,50.00"đặt sai dấu phẩy,"khoảng $300.00"thừa từ ngữ, hoặc"$95.5"thiếu một chữ số thập phân đều là chuỗi không hợp lệ. Nếu dùng.str.contains()hoặc.str.extract(), phần con$300.00vẫn có thể bị nhận nhầm là hợp lệ. - Thao tác: Dùng
s.str.strip().str.fullmatch(pattern, na=False)với mẫuPAT_GIA = r"\$(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{2})?". - Đầu ra: Series kiểu boolean cùng , trả về
Truenếu toàn bộ chuỗi khớp hoàn toàn với mẫu quy chuẩn. - Trường hợp biên: Chuỗi có khoảng trắng ở hai đầu cần được gọt bỏ trước khi đối chiếu, giá trị khuyết thiếu trả về
False.
Lời giải
Cách 1: Kiểm tra toàn bộ chuỗi với fullmatch
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
# Bắt đầu lời giải
gia_test = pd.Series(
["$2,450.00", " $95 ", "$24,50.00", "khoảng $300.00", "$95.5", None]
)
pat_gia = r"\$(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{2})?"
hop_le_cach1 = gia_test.str.strip().str.fullmatch(pat_gia, na=False)
df_kiem_tra_gia = pd.DataFrame({"chuoi_gia": gia_test, "hop_le": hop_le_cach1})
display(df_kiem_tra_gia)
# Kết thúc lời giảiGiải thích Cách 1
.str.strip() chuẩn hóa hai đầu trước khi kiểm tra. .str.fullmatch(pat_gia, na=False) yêu cầu toàn bộ chuỗi còn lại phù hợp mẫu, nên một đoạn con $300.00 nằm trong khoảng $300.00 không đủ làm cả dòng hợp lệ. na=False cho đầu vào thiếu cờ False.
Trong mẫu, \$ khớp dấu đô la thật. Nhánh \d{1,3}(?:,\d{3})+ nhận một đến ba chữ số đầu, theo sau là ít nhất một nhóm dấu phẩy và đúng ba chữ số. Nhánh \d+ nhận số không có dấu nhóm. Dấu | chọn một trong hai dạng đó.
Cụm (?:\.\d{2})? cho phép bỏ hẳn phần thập phân hoặc dùng dấu chấm với đúng hai chữ số. Vì thế $95 hợp lệ, còn $95.5 không hợp lệ. (?:...) gom các thành phần mẫu mà không tạo thêm nhóm bắt để trích dữ liệu.
Output Cách 1
| Vị trí | Chuỗi giá | hop_le |
|---|---|---|
| 0 | $2,450.00 | True |
| 1 | " $95 " | True |
| 2 | $24,50.00 | False |
| 3 | khoảng $300.00 | False |
| 4 | $95.5 | False |
| 5 | Khuyết | False |
Cách 2: Đưa mẫu kiểm tra thành tham số của hàm
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
DEMO_PRICES_TEST = pd.Series(
["$2,450.00", " $95 ", "$24,50.00", "khoảng $300.00", "$95.5", None]
)
PAT_GIA = r"\$(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{2})?"
# Bắt đầu lời giải
def validate_price_fullmatch(s: pd.Series, pattern: str = PAT_GIA) -> pd.Series:
"""Xác thực toàn bộ chuỗi có tuân thủ cấu trúc giá tiền hợp lệ hay không.
Tham số s: Series chuỗi cần kiểm tra.
Tham số pattern: Biểu thức chính quy quy chuẩn cấu trúc số tiền.
Trả về: Series kiểu bool đánh dấu tính hợp lệ của từng dòng.
"""
return s.str.strip().str.fullmatch(pattern, na=False)
ket_qua_hop_le = validate_price_fullmatch(DEMO_PRICES_TEST)
display(pd.DataFrame({"gia_test": DEMO_PRICES_TEST, "hop_le": ket_qua_hop_le}))
# Kết thúc lời giảiGiải thích Cách 2
validate_price_fullmatch(s, pattern=PAT_GIA) dùng mẫu mặc định nhưng cho phép truyền mẫu khác khi quy tắc dữ liệu đổi. Hàm trả Series cờ có cùng , nên có thể dùng trực tiếp để chọn các dòng cần kiểm tra bằng s[~kết_quả].
Giữ mẫu ở một nơi giúp tránh việc hai cột được kiểm tra bằng hai quy tắc hơi khác nhau. Tuy nhiên, thay mẫu là thay tiêu chuẩn chấp nhận dữ liệu, không phải chỉ thay cách viết code. Mẫu đang dùng chưa nhận số âm, ký hiệu tiền khác hoặc dấu phẩy thập phân.
fullmatch kiểm tra toàn chuỗi theo cơ chế của thư viện regex. Không nên diễn giải nó đơn giản thành việc tự gắn hai ký tự ^ và $, vì các neo có quy tắc riêng, chẳng hạn với ký tự xuống dòng. Điều cần nắm ở đây là cả chuỗi phải được mẫu bao phủ.
Output Cách 2
| Vị trí | Chuỗi giá | hop_le |
|---|---|---|
| 0 | $2,450.00 | True |
| 1 | " $95 " | True |
| 2 | $24,50.00 | False |
| 3 | khoảng $300.00 | False |
| 4 | $95.5 | False |
| 5 | Khuyết | False |
So sánh hai cách
Hai cách xác thực cùng một mẫu nên cho cùng sáu cờ. Cách 1 thuận tiện để thử và hiểu từng mảnh regex. Cách 2 giúp tái sử dụng tiêu chuẩn. Kiểm tra hợp lệ chỉ cho biết đúng cú pháp đã quy định, chưa chứng minh mức giá hợp lý hay đúng đơn vị tiền.
Bài 5: Khảo sát tổng quan cột chữ và phân bố độ dài văn bản
Thực hiện khảo sát tổng thể trên một cột văn bản lớn để đánh giá số lượng giá trị khuyết thiếu, đo lường độ dài ký tự và nhận diện các đoạn văn bản quá ngắn ít mang lại giá trị phân tích.
Yêu cầu kỹ thuật:
- Tình huống: Trong tập dữ liệu đánh giá thực tế, một số dòng bị bỏ trống hoặc chỉ chứa một ký tự dấu câu như
".","Ok". Khảo sát độ dài trung vị và các chuỗi ngắn giúp chọn quy tắc xử lý phù hợp với mục tiêu phân tích. - Thao tác: Đếm số lượng
NaN, tính độ dài chuỗi bằng.str.len(), tính giá trị trung vị trên các dòng có dữ liệu và đếm số dòng có độ dài nhỏ hơn ngưỡng quy định. - Đầu ra: Trả về từ điển gồm 4 khóa:
so_nan(int),so_con(int),do_dai_tv(float) vàso_ngan(int). - Trường hợp biên: Dữ liệu mẫu rỗng hoặc chuỗi rỗng có độ dài bằng 0.
Lời giải
Cách 1: Tính riêng từng chỉ số của cột văn bản
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
# Bắt đầu lời giải
so_nan_c1 = int(DEMO_COMMENTS.isna().sum())
c_hop_le = DEMO_COMMENTS.dropna()
so_con_c1 = int(len(c_hop_le))
do_dai_chuoi = c_hop_le.str.len()
do_dai_tv_c1 = float(do_dai_chuoi.median())
so_ngan_c1 = int((do_dai_chuoi < 20).sum())
print(f"Số giá trị khuyết thiếu: {so_nan_c1}")
print(f"Số dòng có dữ liệu: {so_con_c1}")
print(f"Trung vị độ dài ký tự: {do_dai_tv_c1}")
print(f"Số đoạn ngắn hơn 20 ký tự: {so_ngan_c1}")
# Kết thúc lời giảiGiải thích Cách 1
.isna().sum() đếm một ô khuyết. .dropna() lấy 15 dòng còn có dữ liệu, nhưng không loại chuỗi rỗng, dấu chấm hoặc từ Ok. Vì vậy so_con là số dòng không khuyết, chưa phải số đánh giá đủ nội dung cho mọi mục đích phân tích.
.str.len() đo số ký tự trong từng chuỗi. Chuỗi rỗng có độ dài 0 và vẫn tham gia thống kê. .median() lấy trung vị 15 độ dài, được 39.0. Ba chuỗi dưới 20 ký tự là ., Ok và chuỗi rỗng. Phép < 20 không tính một chuỗi dài đúng 20 ký tự.
Dữ liệu tại bước này chưa thay thẻ <br/>, nên các ký tự của thẻ cũng nằm trong độ dài. Với Unicode, số ký tự được thư viện đếm không luôn bằng số ký tự người đọc cảm nhận nếu có dấu tổ hợp. Cần giữ cùng bước chuẩn hóa trước khi so các chỉ số giữa hai tập.
Output Cách 1
| Khóa kết quả | Giá trị |
|---|---|
so_nan | 1 |
so_con | 15 |
do_dai_tv | 39.0 |
so_ngan | 3 |
Cách 2: Đưa ngưỡng độ dài vào hàm khảo sát
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
# Bắt đầu lời giải
def text_overview(comments: pd.Series, nguong_ngan: int = 20) -> dict:
"""Khảo sát số lượng khuyết thiếu, trung vị độ dài và số lượng đoạn văn ngắn.
Tham số comments: Series văn bản cần khảo sát.
Tham số nguong_ngan: Ngưỡng độ dài ký tự dùng để xác định đoạn văn bản ngắn.
Trả về: Từ điển chứa các chỉ số thống kê tổng quan.
"""
so_nan = int(comments.isna().sum())
c = comments.dropna()
so_con = int(len(c))
do_dai = c.str.len()
do_dai_tv = float(do_dai.median()) if so_con > 0 else 0.0
so_ngan = int((do_dai < nguong_ngan).sum())
return {
"so_nan": so_nan,
"so_con": so_con,
"do_dai_tv": do_dai_tv,
"so_ngan": so_ngan,
}
tong_quan_demo = text_overview(DEMO_COMMENTS, nguong_ngan=20)
print("Hồ sơ thống kê văn bản từ text_overview:")
print(tong_quan_demo)
# Kết thúc lời giảiGiải thích Cách 2
nguong_ngan=20 là giá trị mặc định nhưng người gọi có thể đổi mà không sửa thân hàm. Hàm vẫn chỉ tính .str.len() một lần rồi dùng Series độ dài đó cho cả trung vị và số chuỗi ngắn.
Điều kiện if so_con > 0 xử lý trường hợp không còn dòng nào sau khi bỏ khuyết. Đoạn mã quy ước trả do_dai_tv=0.0 trong trường hợp ấy. Cần phân biệt đây là giá trị quy ước của hàm với trung vị của một mẫu rỗng, vốn không được xác định. Khi làm báo cáo, có thể cần trả NaN để tránh nhầm mẫu rỗng với tập toàn chuỗi rỗng.
Từ điển có đúng bốn khóa giúp người gọi kiểm tra hoặc ghép vào báo cáo chất lượng. int và float cố định kiểu vô hướng ở giao diện trả về, còn dữ liệu nguồn được giữ nguyên.
Output Cách 2
| Khóa kết quả | Giá trị |
|---|---|
so_nan | 1 |
so_con | 15 |
do_dai_tv | 39.0 |
so_ngan | 3 |
So sánh hai cách
Cách 1 mở rõ cách tính và dữ liệu ở từng bước. Cách 2 thuận tiện để chạy cùng một khảo sát trên nhiều cột hoặc nhiều ngưỡng. Các con số là thông tin mô tả, chưa tự đưa ra quy tắc xóa văn bản. Cần quyết định mục tiêu phân tích trước khi loại các chuỗi ngắn.
Bài 6: Làm sạch thẻ ngắt dòng HTML đã biết và kiểm chứng bảo toàn độ dài
Thực hiện thay thế đúng chuỗi thẻ ngắt dòng <br/> trong văn bản bằng một dấu cách, bảo toàn tính nguyên vẹn của văn bản gốc và kiểm chứng bằng công thức toán học về độ dài ký tự.
Yêu cầu kỹ thuật:
- Tình huống: Hệ thống Airbnb lưu các vị trí xuống dòng bằng chuỗi cố định
"<br/>". Mỗi thẻ này gồm đúng 5 ký tự. Khi thay một thẻ bằng 1 dấu cách, độ dài của văn bản sẽ giảm đi đúng 4 ký tự và số lượng dấu cách tăng thêm đúng 1. - Thao tác: Đếm số dòng chứa
"<br/>", dùng.str.replace("<br/>", " ", regex=False)để thay thế, bảo toàn giá trị khuyết thiếu và . - Đầu ra: Trả về từ điển gồm hai khóa:
so_br(int) vàc_sach(Series chuỗi đã được làm sạch). - Trường hợp biên: Không thay thế các thẻ HTML khác ngoài quy cách như
"<br>"hay"<p>", giữ nguyên giá trịNaN.
Lời giải
Cách 1: Đếm dòng có thẻ rồi thay đúng chuỗi br
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
# Bắt đầu lời giải
so_br_c1 = int(DEMO_COMMENTS.str.contains("<br/>", regex=False, na=False).sum())
c_sach_c1 = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
con_br = int(c_sach_c1.str.contains("<br/>", regex=False, na=False).sum())
print(f"Số dòng chứa br ban đầu: {so_br_c1}")
print(f"Số dòng chứa br sau làm sạch: {con_br}")
# Kết thúc lời giảiGiải thích Cách 1
str.contains("<br/>", regex=False, na=False) trả một cờ cho mỗi dòng. .sum() vì thế cho số dòng có ít nhất một thẻ, không phải tổng số thẻ. DEMO có ba dòng như vậy, trong đó dòng 6 chứa hai thẻ.
str.replace("<br/>", " ", regex=False) thay mỗi lần xuất hiện đúng chuỗi đã biết bằng một dấu cách. Những thẻ khác như <br> giữ nguyên. Việc chọn dấu cách giúp hai từ ở hai phía không bị dính liền sau khi bỏ thẻ.
Phép kiểm tra sau thay thế cho biết còn bao nhiêu dòng chứa đúng thẻ cũ. Kết quả 0 xác nhận đã xử lý hết dạng đang xét. Nó không chứng minh văn bản không còn mọi thẻ HTML khác.
Output Cách 1
| Chỉ số | Giá trị |
|---|---|
Dòng có ít nhất một <br/> trước xử lý | 3 |
| Dòng còn đúng thẻ ấy sau xử lý | 0 |
| Tổng số thẻ trong ba dòng | 4 |
Cách 2: Gói quy tắc và kiểm tra mức giảm độ dài
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
# Bắt đầu lời giải
def clean_br(comments: pd.Series) -> dict:
"""Thay thế chính xác thẻ <br/> bằng một khoảng trắng và thống kê số dòng chứa thẻ.
Tham số comments: Series chuỗi nguồn cần làm sạch.
Trả về: Từ điển chứa số dòng phát hiện so_br và Series đã làm sạch c_sach.
"""
so_br = int(comments.str.contains("<br/>", regex=False, na=False).sum())
c_sach = comments.str.replace("<br/>", " ", regex=False)
return {"so_br": so_br, "c_sach": c_sach}
ket_qua_br = clean_br(DEMO_COMMENTS)
print(f"Số dòng có thẻ br: {ket_qua_br['so_br']}")
print("Mẫu dòng số 6 sau khi làm sạch:")
print(f"'{ket_qua_br['c_sach'].iloc[6]}'")
# Kiểm chứng tính giảm độ dài: Mỗi lần xuất hiện giảm đúng 4 ký tự
c_khong_nan = DEMO_COMMENTS.dropna()
so_the_tong = c_khong_nan.str.count("<br/>").sum()
do_dai_giam = (
c_khong_nan.str.len().sum() - ket_qua_br["c_sach"].dropna().str.len().sum()
)
print(
f"Tổng số thẻ br xuất hiện: {so_the_tong}, Độ dài giảm: {do_dai_giam} ký tự (Khớp 4 * {so_the_tong} = {4 * so_the_tong})"
)
# Kết thúc lời giảiGiải thích Cách 2
clean_br trả cả số dòng có thẻ lẫn Series đã làm sạch, giúp giữ phép biến đổi cùng thông tin kiểm tra. Trong phần kiểm chứng, .str.count("<br/>") đếm số lần xuất hiện trong mỗi dòng, rồi .sum() cộng lại thành tổng bốn thẻ. Mẫu này không có ký tự regex làm đổi cách hiểu chuỗi cần tìm.
Mỗi thẻ gồm năm ký tự và được thay bằng một dấu cách, nên mỗi lần thay giảm bốn ký tự. Bốn lần thay làm tổng độ dài giảm 16. Đếm dòng với contains và đếm lần xuất hiện với count là hai phép đo khác nhau, dù cùng làm việc trên một chuỗi thẻ.
Đối chiếu độ dài là một cách kiểm tra tác động cụ thể của phép biến đổi. Nó chưa chứng minh nội dung được hiểu đúng về ngữ nghĩa. Dòng thiếu dữ liệu vẫn thiếu sau xử lý, không bị biến thành chữ None hoặc chuỗi rỗng.
Output Cách 2
| Đại lượng | Output |
|---|---|
so_br | 3 |
| Tổng số thẻ | 4 |
| Tổng độ dài giảm | 16 ký tự |
| Dòng 6 sau xử lý | Nice host. Near Movistar Arena! Recommended. |
So sánh hai cách
Cách 1 phù hợp để quan sát một phép thay thế đã biết. Cách 2 bổ sung hàm dùng lại và một kiểm tra độc lập bằng độ dài. Cả hai chỉ xử lý đúng chuỗi <br/>. Làm sạch HTML tổng quát cần công cụ phân tích cấu trúc và quy tắc giữ nội dung rõ ràng.
Bài 7: Xây dựng quy tắc từ khoá nhận diện ngôn ngữ văn bản
Thiết lập một quy tắc xấp xỉ sử dụng biểu thức chính quy từ khoá để phân loại sơ bộ các đoạn đánh giá bằng tiếng Tây Ban Nha và đo lường tỷ lệ bao phủ của quy tắc.
Yêu cầu kỹ thuật:
- Tình huống: Santiago là thành phố sử dụng tiếng Tây Ban Nha. Có thể xây dựng một bộ từ khoá đặc trưng ban đầu gồm
PAT_ES = r"ción|ñ|muy|excelente|departamento"để làm mốc phân loại cơ sở. - Thao tác: Kiểm tra sự xuất hiện của mẫu không phân biệt hoa thường bằng
comments.str.contains(pattern, case=False, na=False). - Đầu ra: Trả về từ điển gồm hai khóa:
la_es(Series boolean cùng ) vàty_le_es(float biểu thị tỷ lệ dòng thoả mãn trên tổng số dòng đầu vào). - Trường hợp biên: Giá trị khuyết thiếu nhận giá trị
False, phân tích rõ các trường hợp nhận diện nhầm (như câu tiếng Anh chứa từ mượn) và trường hợp bỏ sót (câu tiếng Tây Ban Nha không chứa từ khoá trong danh sách).
Lời giải
Cách 1: Tìm các dấu hiệu bằng contains
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
# Bắt đầu lời giải
c_sach_demo = DEMO_COMMENTS.dropna().str.replace("<br/>", " ", regex=False)
la_es_c1 = c_sach_demo.str.contains(PAT_ES, case=False, na=False)
ty_le_es_c1 = float(la_es_c1.sum() / len(c_sach_demo))
print(f"Số dòng nhận diện tiếng Tây Ban Nha: {la_es_c1.sum()}/{len(c_sach_demo)}")
print(f"Tỷ lệ bao phủ: {ty_le_es_c1:.3f}")
# Kết thúc lời giảiGiải thích Cách 1
PAT_ES dùng dấu | để chọn một trong các đoạn ción, ñ, muy, excelente, departamento. .str.contains(..., case=False, na=False) chỉ cần một đoạn khớp ở bất kỳ vị trí nào để trả True. case=False cho phép nhận cả Excelente và excelente.
Ví dụ bỏ ô khuyết rồi thay <br/> trước khi phân loại, nên mẫu số là 15, không phải toàn bộ 16 dòng ban đầu. Chuỗi rỗng vẫn nằm trong mẫu số và không khớp. Bảy cờ đúng tạo tỷ lệ 7 / 15.
Tỷ lệ này đo phần văn bản khớp một quy tắc từ khóa, không đo độ chính xác nhận diện ngôn ngữ. Câu The departamento was great, near the mall. khớp dù mạch câu chủ yếu là tiếng Anh. Câu Todo perfecto. Volvería sin duda. không khớp dù là tiếng Tây Ban Nha. Hai ví dụ cho thấy cả nhận nhầm lẫn bỏ sót.
Output Cách 1
| Kết quả trên c_sach_demo | Số dòng |
|---|---|
Khớp quy tắc PAT_ES | 7 |
| Không khớp quy tắc | 8 |
| Tổng dòng không khuyết được xét | 15 |
Tỷ lệ 7 / 15 làm tròn ba chữ số là 0.467.
Cách 2: Trả cờ từng dòng và tỷ lệ từ một hàm
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
# Bắt đầu lời giải
def language_flag(comments: pd.Series, pattern: str = PAT_ES) -> dict:
"""Tạo cờ nhận diện ngôn ngữ theo quy tắc từ khoá và tính tỷ lệ bao phủ.
Tham số comments: Series chuỗi cần phân loại.
Tham số pattern: Biểu thức chính quy chứa các từ khoá đặc trưng.
Trả về: Từ điển chứa cờ la_es kiểu bool và tỷ lệ ty_le_es.
"""
la_es = comments.str.contains(pattern, case=False, na=False)
ty_le_es = float(la_es.sum() / len(comments)) if len(comments) > 0 else 0.0
return {"la_es": la_es, "ty_le_es": ty_le_es}
ket_qua_es = language_flag(c_sach_demo)
print(
f"Kết quả phân loại ngôn ngữ: {ket_qua_es['la_es'].sum()} dòng, tỷ lệ {ket_qua_es['ty_le_es']:.3f}"
)
# Kết thúc lời giảiGiải thích Cách 2
language_flag nhận cả Series và mẫu, nên có thể thử một bộ dấu hiệu mới mà không thay các bước tính tỷ lệ. la_es giữ đầu vào để các phép lọc phía sau chọn đúng văn bản. la_es.sum() đếm cờ đúng, còn len(comments) là mẫu số của lần gọi.
Nhánh if len(comments) > 0 else 0.0 tránh chia cho 0 khi đầu vào rỗng. Số 0 khi đó là quy ước trả về của hàm, không phải bằng chứng không có tiếng Tây Ban Nha trong một quần thể đã quan sát.
Lần gọi đang dùng c_sach_demo đã bỏ khuyết nên giữ tỷ lệ như Cách 1. Nếu gọi hàm trên Series gốc có một ô khuyết, na=False làm cờ ở ô ấy sai nhưng len vẫn tính dòng đó, khiến tỷ lệ thành 7 / 16. Cần chọn và công bố mẫu số trước khi so sánh các kết quả.
Output Cách 2
| Kết quả trên c_sach_demo | Số dòng |
|---|---|
Khớp quy tắc PAT_ES | 7 |
| Không khớp quy tắc | 8 |
| Tổng dòng không khuyết được xét | 15 |
Tỷ lệ 7 / 15 làm tròn ba chữ số là 0.467.
So sánh hai cách
Hai cách dùng cùng quy tắc xấp xỉ. Cách 2 giúp giữ cờ và tỷ lệ trong một cấu trúc dùng lại, chưa biến quy tắc thành mô hình nhận diện ngôn ngữ chính xác. Nên gọi hai nhóm là khớp và không khớp quy tắc khi diễn giải, thay vì mặc nhiên coi đó là hai ngôn ngữ đã được xác nhận.
Bài 8: So sánh trung vị độ dài văn bản theo nhóm ngôn ngữ
Thực hiện phân nhóm dữ liệu văn bản theo cờ nhận diện ngôn ngữ và so sánh chỉ số độ dài trung vị của hai nhóm mà không sử dụng vòng lặp Python.
Yêu cầu kỹ thuật:
- Tình huống: Người phân tích cần tìm hiểu xem các đánh giá bằng ngôn ngữ bản địa có xu hướng viết dài hơn hoặc ngắn hơn so với các đánh giá bằng ngôn ngữ khác hay không.
- Thao tác: Dùng mặt nạ boolean
flagđể lọc hai tập concomments[flag]vàcomments[~flag], sau đó gọi phương thức.str.len().median()trên từng tập. - Đầu ra: Trả về từ điển gồm hai khóa:
len_es(float) vàlen_khac(float). - Trường hợp biên: Series đầu vào không chứa giá trị khuyết thiếu và mỗi nhóm có ít nhất một dòng dữ liệu.
Lời giải
Cách 1: Tính độ dài một lần rồi lọc hai nhóm
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
def language_flag(comments: pd.Series, pattern: str = PAT_ES) -> dict:
"""Tạo cờ nhận diện ngôn ngữ theo quy tắc từ khoá và tính tỷ lệ bao phủ.
Tham số comments: Series chuỗi cần phân loại.
Tham số pattern: Biểu thức chính quy chứa các từ khoá đặc trưng.
Trả về: Từ điển chứa cờ la_es kiểu bool và tỷ lệ ty_le_es.
"""
la_es = comments.str.contains(pattern, case=False, na=False)
ty_le_es = float(la_es.sum() / len(comments)) if len(comments) > 0 else 0.0
return {"la_es": la_es, "ty_le_es": ty_le_es}
ket_qua_es = language_flag(c_sach_demo)
flag_es = ket_qua_es["la_es"]
flag_es_demo = flag_es.copy()
# Bắt đầu lời giải
do_dai_series = c_sach_demo.str.len()
flag_es = ket_qua_es["la_es"]
len_es_c1 = float(do_dai_series[flag_es].median())
len_khac_c1 = float(do_dai_series[~flag_es].median())
print(f"Trung vị độ dài nhóm es: {len_es_c1} ký tự")
print(f"Trung vị độ dài nhóm khác: {len_khac_c1} ký tự")
# Kết thúc lời giảiGiải thích Cách 1
c_sach_demo.str.len() đo độ dài của văn bản đã làm sạch thẻ. flag_es là Series cờ cùng . Dùng do_dai_series[flag_es] lấy độ dài của các dòng khớp, còn ~flag_es đảo True và False để chọn các dòng không khớp.
.median() sắp theo giá trị độ dài để tìm điểm giữa, không dựa vào thứ tự văn bản trong bảng. Nhóm bảy dòng khớp có trung vị 47.0. Nhóm tám dòng không khớp có trung vị là trung bình hai độ dài giữa, được 34.5.
Mặt nạ cần có tương ứng với cột văn bản. Không lấy một mảng cờ từ bảng đã sắp khác thứ tự rồi coi nó tự khớp theo vị trí. Đây là lý do giữ các Series và chỉ mục xuyên suốt quy trình.
Output Cách 1
| Nhóm cờ | Số dòng | Trung vị độ dài |
|---|---|---|
True | 7 | 47.0 |
False | 8 | 34.5 |
Cách 2: Viết hàm so sánh độ dài theo Series cờ
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
def language_flag(comments: pd.Series, pattern: str = PAT_ES) -> dict:
"""Tạo cờ nhận diện ngôn ngữ theo quy tắc từ khoá và tính tỷ lệ bao phủ.
Tham số comments: Series chuỗi cần phân loại.
Tham số pattern: Biểu thức chính quy chứa các từ khoá đặc trưng.
Trả về: Từ điển chứa cờ la_es kiểu bool và tỷ lệ ty_le_es.
"""
la_es = comments.str.contains(pattern, case=False, na=False)
ty_le_es = float(la_es.sum() / len(comments)) if len(comments) > 0 else 0.0
return {"la_es": la_es, "ty_le_es": ty_le_es}
ket_qua_es = language_flag(c_sach_demo)
flag_es = ket_qua_es["la_es"]
flag_es_demo = flag_es.copy()
# Bắt đầu lời giải
def median_length_by_flag(comments: pd.Series, flag: pd.Series) -> dict:
"""So sánh độ dài trung vị giữa hai nhóm phân loại theo cờ boolean.
Tham số comments: Series chuỗi không chứa giá trị khuyết thiếu.
Tham số flag: Series kiểu bool cùng chỉ mục với comments.
Trả về: Từ điển chứa trung vị độ dài len_es và len_khac.
"""
len_es = float(comments[flag].str.len().median()) if flag.any() else 0.0
len_khac = float(comments[~flag].str.len().median()) if (~flag).any() else 0.0
return {"len_es": len_es, "len_khac": len_khac}
ket_qua_do_dai = median_length_by_flag(c_sach_demo, flag_es)
print("Kết quả so sánh trung vị độ dài từ median_length_by_flag:")
print(ket_qua_do_dai)
# Kết thúc lời giảiGiải thích Cách 2
median_length_by_flag nhận văn bản và cờ thành hai đối số để dùng lại cho bất kỳ quy tắc phân nhóm nào. comments[flag].str.len().median() chọn đúng nhóm rồi đo độ dài và tính trung vị. (~flag).any() kiểm tra có ít nhất một dòng trong nhóm đối diện hay không.
Các nhánh điều kiện trong hàm trả 0.0 khi không có dòng của một nhóm. Đây là giá trị dự phòng do mã lựa chọn, không phải trung vị toán học của mẫu rỗng. Theo giả thiết của bài, mỗi nhóm đều có dữ liệu nên hai nhánh dự phòng chưa được dùng.
Hàm gọi .str.len() trên hai tập con thay vì tính một Series độ dài chung như Cách 1. Hai cách có cùng kết quả trên dữ liệu đã cho. Việc đóng gói giúp tái sử dụng, nhưng không đủ để khẳng định hiệu năng tốt hơn.
Output Cách 2
| Nhóm cờ | Số dòng | Trung vị độ dài |
|---|---|---|
True | 7 | 47.0 |
False | 8 | 34.5 |
So sánh hai cách
Cách 1 thuận tiện khi cần dùng lại cột độ dài cho nhiều thống kê. Cách 2 có giao diện rõ để tích hợp vào quy trình phân nhóm. Hai trung vị chỉ mô tả những nhóm do quy tắc từ khóa tạo ra trong mẫu này. Chưa thể suy ra người dùng một ngôn ngữ nói chung viết dài hơn.
Bài 9: Trích xuất thực thể vị trí đứng sau từ chỉ phương hướng
Sử dụng biểu thức chính quy có nhóm bắt để trích xuất từ đầu tiên đứng ngay sau các cụm từ chỉ phương hướng và khoảng cách lân cận trong đánh giá phòng.
Yêu cầu kỹ thuật:
- Tình huống: Khách lưu trú thường đề cập đến vị trí xung quanh qua các cụm từ như
"cerca de","cerca del","cerca de la","near","near the". Ta cần trích xuất từ đơn đứng liền sau cụm từ này để thống kê các địa danh thường gặp. - Thao tác: Dùng
s.str.extract(pattern, expand=False)với mẫu chứa nhóm không bắt(?:...)cho cụm từ chỉ hướng và nhóm bắt([A-Za-zÁ-Úá-úñÑ]+)cho từ mục tiêu. - Đầu ra: Trả về từ điển gồm hai khóa:
gan(Series chuỗi kết quả trích xuất, giữ nguyên và chữ hoa thường) vàso_trich(số nguyên biểu thị số dòng trích xuất thành công). - Trường hợp biên: Mẫu phân biệt hoa thường theo đúng định nghĩa, các dòng không khớp hoặc giá trị khuyết thiếu trả về
NaN.
Lời giải
Cách 1: Trích nhóm bắt đầu tiên sau cụm chỉ vị trí
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_GAN = r"(?:cerca de la |cerca del |cerca de |near the |near )([A-Za-zÁ-Úá-úñÑ]+)"
# Bắt đầu lời giải
c_sach_full = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
gan_c1 = c_sach_full.str.extract(PAT_GAN, expand=False)
so_trich_c1 = int(gan_c1.notna().sum())
print(f"Số dòng trích xuất thành công: {so_trich_c1}")
print("Năm giá trị trích xuất đầu tiên:")
print(gan_c1.dropna().head(5).tolist())
# Kết thúc lời giảiGiải thích Cách 1
str.extract(PAT_GAN, expand=False) tìm lần khớp đầu tiên trong mỗi dòng và trả nội dung của nhóm bắt. Các cụm cerca de la, cerca del, cerca de, near the, near nằm trong nhóm không bắt (?:...), nên không trở thành dữ liệu đầu ra.
Nhóm ([A-Za-zÁ-Úá-úñÑ]+) lấy một hoặc nhiều ký tự trong các khoảng đã viết. Mẫu hỗ trợ các từ minh họa ở đây nhưng không bao phủ mọi chữ cái Unicode hoặc mọi cách viết tên riêng. expand=False trả Series khi có một nhóm bắt, giúp giữ cùng với cột nguồn.
Mẫu phân biệt hoa thường. Vì vậy Near Movistar Arena với N hoa không được nhận bởi nhánh near, còn cerca de Movistar Arena cho từ Movistar. Chỉ lấy một từ cũng có nghĩa tên nhiều từ bị cắt. Đếm ô không khuyết đo số lần quy tắc trích được chữ, chưa đo số địa danh được xác minh.
Output Cách 1
| gốc | Từ trích xuất |
|---|---|
| 0 | metro |
| 1 | metro |
| 5 | todo |
| 8 | estación |
| 10 | muchos |
| 11 | mall |
| 12 | centro |
| 13 | metro |
| 15 | Movistar |
so_trich = 9. Năm giá trị đầu sau khi bỏ khuyết là metro, metro, todo, estación, muchos.
Cách 2: Gói quy tắc trích và số dòng khớp trong một hàm
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_GAN = r"(?:cerca de la |cerca del |cerca de |near the |near )([A-Za-zÁ-Úá-úñÑ]+)"
# Bắt đầu lời giải
def extract_near(comments: pd.Series, pattern: str = PAT_GAN) -> dict:
"""Trích xuất từ đầu tiên đứng ngay sau cụm từ chỉ vị trí lân cận.
Tham số comments: Series văn bản cần trích xuất.
Tham số pattern: Biểu thức chính quy chứa đúng một nhóm bắt thực thể.
Trả về: Từ điển chứa Series gan và số lượng dòng trích thành công so_trich.
"""
gan = comments.str.extract(pattern, expand=False)
so_trich = int(gan.notna().sum())
return {"gan": gan, "so_trich": so_trich}
ket_qua_trich = extract_near(c_sach_full)
print(f"Tổng số dòng trích xuất được: {ket_qua_trich['so_trich']}")
print(f"Ví dụ dòng 0 (metro): {ket_qua_trich['gan'].iloc[0]}")
print(f"Ví dụ dòng 5 (todo): {ket_qua_trich['gan'].iloc[5]}")
print(f"Ví dụ dòng 15 (Movistar): {ket_qua_trich['gan'].iloc[15]}")
# Kết thúc lời giảiGiải thích Cách 2
Hàm extract_near nhận mẫu qua đối số để có thể điều chỉnh quy tắc độc lập với logic đếm. gan.notna().sum() chỉ cộng những dòng có kết quả trích, còn các dòng không khớp tiếp tục mang giá trị khuyết trong Series gan.
Thứ tự các nhánh tiền tố có ý nghĩa. Đặt cerca de la trước cerca de giúp mẫu dài được thử trước, nên đoạn cerca de la estación lấy estación thay vì lấy nhầm la từ một nhánh ngắn hơn. Dấu cách cuối mỗi tiền tố cũng là một phần phải khớp.
Kết quả vẫn có các từ todo và muchos, vốn không phải tên địa điểm. Mẫu chỉ nhận cấu trúc chữ theo quy tắc, không hiểu ngữ nghĩa. Đó là lý do cần xem bảng kết quả và có bước lọc hoặc xác minh ở bài tiếp theo.
Output Cách 2
| gốc | Từ trích xuất |
|---|---|
| 0 | metro |
| 1 | metro |
| 5 | todo |
| 8 | estación |
| 10 | muchos |
| 11 | mall |
| 12 | centro |
| 13 | metro |
| 15 | Movistar |
Các lệnh minh họa riêng in dòng 0 là metro, dòng 5 là todo, dòng 15 là Movistar.
So sánh hai cách
Cách 1 thuận tiện để thử trực tiếp một regex và đọc các giá trị đầu. Cách 2 giữ quy tắc cùng số đo khớp trong một hàm dùng lại. Cả hai cần kiểm tra các trường hợp không khớp, từ không phải địa danh và tên nhiều từ trước khi dùng đầu ra làm dữ liệu vị trí.
Bài 10: Tổng hợp tần suất địa danh và phát hiện trường hợp khớp nhầm
Tổng hợp bảng tần suất các từ được trích xuất từ bài tập trước, nhận diện các trường hợp khớp nhầm cấu trúc ngữ pháp và lọc bỏ chúng thông qua danh sách từ dừng.
Yêu cầu kỹ thuật:
- Tình huống: Biểu thức chính quy nhận diện chuẩn cấu trúc
"cerca de X"nhưng không hiểu nghĩa của từ. Trong tiếng Tây Ban Nha,"todo"nghĩa là "mọi thứ" và"muchos"nghĩa là "nhiều", hoàn toàn không phải tên địa danh. - Thao tác: Chuyển toàn bộ từ trích xuất sang chữ thường, loại bỏ giá trị khuyết thiếu
NaN, loại bỏ các từ nằm trong danh sách dừngbo_quavà lấyntừ xuất hiện nhiều nhất bằng.value_counts().head(n). - Đầu ra: Trả về Series có là từ (chữ thường) và giá trị là số lần xuất hiện (int) sắp xếp giảm dần.
- Trường hợp biên: Tham số
bo_quanhận một tuple các chuỗi viết thường, mặc định là rỗng().
Lời giải
Cách 1: Quan sát bảng tần suất trước khi quyết định lọc
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
gan_demo = pd.Series(
[
"metro",
"Metro",
None,
"todo",
"estación",
"metro",
"Movistar",
"muchos",
"movistar",
]
)
gan_sach_c1 = gan_demo.dropna().str.lower()
tan_suat_c1 = gan_sach_c1.value_counts().head(5)
print("Bảng tần suất ban đầu chưa lọc từ dừng:")
print(tan_suat_c1)
gan_khong_tu_dung = gan_sach_c1.loc[~gan_sach_c1.isin(["todo", "muchos"])]
tan_suat_da_loc = gan_khong_tu_dung.value_counts().head(2)
print("Hai từ có tần suất cao nhất sau khi lọc:")
print(tan_suat_da_loc)
# Kết thúc lời giảiGiải thích Cách 1
gan_demo là một mẫu nhỏ để khảo sát phép đếm. .dropna() bỏ ô không trích được từ. .str.lower() gộp metro với Metro, đồng thời gộp Movistar với movistar, để không chia một dạng chữ thành nhiều khóa đếm.
.value_counts() đếm mỗi giá trị phân biệt và xếp theo số lần giảm dần. .head(5) lấy năm giá trị đầu, không phải năm dòng nguồn. Các từ hòa một lần xuất hiện chưa được yêu cầu xếp theo bảng chữ cái, nên không nên dựa vào thứ tự hòa khi viết quy tắc xử lý sau đó.
Bảng chưa lọc làm lộ todo và muchos. Cần phân biệt bước chẩn đoán này với đầu ra cuối đã loại từ dừng. Có tần suất xuất hiện không chứng minh một từ là tên địa danh. Bước tiếp theo phải áp dụng danh sách đã quyết định loại.
Output Cách 1
| Từ | Số lần |
|---|---|
metro | 3 |
movistar | 2 |
todo | 1 |
estación | 1 |
muchos | 1 |
Cách 2: Thêm danh sách từ dừng và giới hạn số kết quả
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
gan_demo = pd.Series(
[
"metro",
"Metro",
None,
"todo",
"estación",
"metro",
"Movistar",
"muchos",
"movistar",
]
)
# Bắt đầu lời giải
def top_places(gan: pd.Series, n: int = 8, bo_qua: tuple = ()) -> pd.Series:
"""Lập bảng tần suất các từ trích xuất, hỗ trợ loại trừ danh sách từ dừng.
Tham số gan: Series chuỗi kết quả trích xuất.
Tham số n: Số lượng từ phổ biến nhất cần lấy.
Tham số bo_qua: Tuple chứa các từ dừng cần loại bỏ khỏi bảng xếp hạng.
Trả về: Series tần suất có chỉ mục là từ viết thường, sắp xếp giảm dần.
"""
s = gan.dropna().str.lower()
if bo_qua:
s = s[~s.isin(bo_qua)]
return s.value_counts().head(n)
top_chua_loc = top_places(gan_demo, n=5)
print("Top 5 từ trước khi lọc từ dừng:")
print(top_chua_loc)
top_da_loc = top_places(gan_demo, n=2, bo_qua=("todo", "muchos"))
print("\nTop 2 địa danh thực tế sau khi lọc todo và muchos:")
print(top_da_loc)
# Kết thúc lời giảiGiải thích Cách 2
bo_qua=() mặc định là tuple rỗng, nghĩa là chưa loại từ nào. Nếu người gọi truyền các từ dừng, s.isin(bo_qua) đánh dấu từ thuộc danh sách và ~ đảo mặt nạ để chỉ giữ phần còn lại. Việc đổi chữ thường được làm trước, nên danh sách từ dừng cũng cần ở dạng chữ thường.
Sau lọc, .value_counts().head(n) mới đếm và giới hạn. Với n=2, bo_qua=("todo", "muchos"), kết quả có metro ba lần và movistar hai lần. estación không bị loại, nhưng đứng ngoài hai vị trí đầu vì chỉ có một lần.
Việc bỏ hai từ dừng không xác nhận mọi từ còn lại là tên địa điểm thật. Chẳng hạn metro là một danh từ chỉ phương tiện hoặc nhà ga theo ngữ cảnh, còn Movistar mới chỉ là một phần tên đã trích. Kết quả cần được kiểm tra theo mục đích phân tích.
Output Cách 2
Lần gọi chưa truyền từ dừng trả bảng năm từ như Cách 1. Lần gọi có lọc và lấy hai từ đầu trả:
| Từ | Số lần |
|---|---|
metro | 3 |
movistar | 2 |
So sánh hai cách
Cách 1 giúp phát hiện các từ cần rà soát trước khi chốt danh sách lọc. Cách 2 thực hiện quy tắc lọc có thể lặp lại bằng đối số hàm. Chỉ bước quan sát bảng chưa lọc chưa hoàn thành yêu cầu đầu ra có từ dừng. Khi cần kết quả cuối, phải áp dụng cùng danh sách và cùng giới hạn ở cả hai cách.
Bài 11: Phân rã danh sách tiện nghi JSON bằng phương thức explode
Thực hiện đọc và phân rã cột thuộc tính tiện nghi lưu dưới dạng chuỗi JSON thành các dòng dữ liệu riêng biệt bằng phương thức explode, chú ý xử lý các trường hợp tên tiện nghi có chứa dấu phẩy.
Yêu cầu kỹ thuật:
- Tình huống: Cột
amenitiestrong bảng dữ liệu chỗ ở lưu một mảng JSON dưới dạng chuỗi văn bản như'["Wifi", "Air conditioning"]'. Một số mục tiện nghi chứa sẵn dấu phẩy trong tên như"Wifi, free", nên tuyệt đối không được dùng phương thức cắt chuỗi bằng dấu phẩy thông thường. - Thao tác: Dùng
json.loadstrong hàm ánh xạ để chuyển chuỗi thành danh sách Python, sau đó dùngSeries.explode()để phân rã mỗi phần tử của danh sách thành một dòng độc lập, giữ nguyên mã chỗ ở. - Đầu ra: Trả về một tuple gồm hai Series:
(ds, muc), trong đódslà Series các danh sách Python vàmuclà Series các mục tiện nghi sau khi phân rã đã loại bỏ phần tử rỗng. - Trường hợp biên: Dòng chứa danh sách rỗng
'[]'hoặc giá trị khuyết thiếu cần được xử lý an toàn mà không làm dừng chương trình.
Lời giải
Cách 1: Giải mã JSON rồi tách từng phần tử danh sách
python
# Dữ liệu mẫu và các hàm hỗ trợ
import json
import pandas as pd
DEMO_LISTINGS = pd.DataFrame(
{
"amenities": [
'["Wifi", "Air conditioning"]',
'["Wifi, free", "Kitchen"]',
"[]",
'["Kitchen"]',
'["Heating"]',
]
},
index=[101, 102, 103, 104, 105],
)
# Bắt đầu lời giải
ds_c1 = DEMO_LISTINGS["amenities"].map(json.loads)
muc_c1 = ds_c1.explode().dropna()
print(f"Tổng số chỗ ở khảo sát: {len(ds_c1)}")
print(f"Tổng số lượt tiện nghi sau khi explode: {len(muc_c1)}")
print("Bảng đếm tiện nghi phổ biến Cách 1:")
print(muc_c1.value_counts())
# Kết thúc lời giảiGiải thích Cách 1
map(json.loads) gọi bộ giải mã JSON cho từng chuỗi trong cột. Chuỗi '["Wifi, free", "Kitchen"]' trở thành một danh sách có đúng hai phần tử. Dấu phẩy bên trong tên Wifi, free được bộ giải mã giữ nguyên vì nằm trong một chuỗi JSON, khác với dấu phân cách hai phần tử.
.explode() tạo một dòng cho mỗi phần tử danh sách và lặp lại của chỗ ở gốc. Với danh sách rỗng, pandas tạo một ô khuyết trong kết quả trung gian. .dropna() loại ô ấy, nên chỉ còn sáu dòng tiện nghi từ năm chỗ ở.
Cách cơ bản giả thiết các chuỗi đều là mảng JSON hợp lệ. Nếu gặp chuỗi JSON sai hoặc ô thiếu, json.loads trực tiếp có thể phát sinh lỗi. Bảng mẫu không có những đầu vào đó, nên mã chạy được nhưng chưa đủ để kết luận đã kiểm soát mọi trường hợp biên.
Output Cách 1
| Tiện nghi | Số lần xuất hiện |
|---|---|
| Kitchen | 2 |
| Wifi | 1 |
| Air conditioning | 1 |
| Wifi, free | 1 |
| Heating | 1 |
Có 5 chỗ ở và 6 dòng tiện nghi sau explode().dropna().
Cách 2: Tách hàm giải mã và trả cả danh sách lẫn bảng dài
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import json
import pandas as pd
DEMO_LISTINGS = pd.DataFrame(
{
"amenities": [
'["Wifi", "Air conditioning"]',
'["Wifi, free", "Kitchen"]',
"[]",
'["Kitchen"]',
'["Heating"]',
]
},
index=[101, 102, 103, 104, 105],
)
# Bắt đầu lời giải
def parse_amenities_explode(
df: pd.DataFrame, col: str = "amenities"
) -> tuple[pd.Series, pd.Series]:
"""Giải mã chuỗi JSON và phân rã các mục tiện nghi thành từng dòng độc lập.
Tham số df: DataFrame chứa dữ liệu chỗ ở.
Tham số col: Tên cột chứa chuỗi JSON mảng tiện nghi.
Trả về: Tuple gồm Series danh sách Python ds và Series các mục tiện nghi muc.
"""
def safe_loads(val):
if pd.isna(val):
return []
try:
return json.loads(val)
except Exception:
return []
ds = df[col].map(safe_loads)
muc = ds.explode().dropna()
return ds, muc
ds_demo, muc_demo = parse_amenities_explode(DEMO_LISTINGS)
print("Kết quả phân tích tiện nghi từ parse_amenities_explode:")
print(f"Số lượng chỗ ở có danh sách: {len(ds_demo)}")
print(
f"Kiểm tra có tên tiện nghi chứa dấu phẩy: {muc_demo.str.contains(',', regex=False).any()}"
)
display(muc_demo.to_frame(name="ten_tien_nghi"))
# Kết thúc lời giảiGiải thích Cách 2
safe_loads được định nghĩa bên trong hàm để xử lý mỗi ô. Với mã đang minh họa, ô khuyết hoặc chuỗi gây lỗi được quy về danh sách rỗng. ds giữ một danh sách cho mỗi chỗ ở. Biến muc là Series dài sau khi tách và bỏ ô khuyết. Trả cả hai giúp chọn đúng đơn vị phân tích ở các bài sau.
Cách xử lý lỗi này cho quy trình tiếp tục chạy nhưng làm mất phân biệt giữa không liệt kê tiện nghi, thiếu dữ liệu và JSON lỗi. Khi dữ liệu thật cần kiểm toán, nên báo hoặc lưu cờ lỗi riêng thay vì âm thầm coi mọi trường hợp là danh sách rỗng. Đồng thời phải kiểm tra JSON giải mã thành danh sách, vì JSON hợp lệ cũng có thể là một số hoặc từ điển.
muc_demo.str.contains(",", regex=False).any() trả True, xác nhận vẫn có tên tiện nghi chứa dấu phẩy. to_frame(name="ten_tien_nghi") đổi Series thành bảng một cột để hiển thị đúng các hàng có cùng chỗ ở.
Output Cách 2
| chỗ ở | Tên tiện nghi |
|---|---|
| 101 | Wifi |
| 101 | Air conditioning |
| 102 | Wifi, free |
| 102 | Kitchen |
| 104 | Kitchen |
| 105 | Heating |
Kiểm tra tên có chứa dấu phẩy trả True. Chỗ ở 103 có danh sách rỗng nên không xuất hiện trong bảng dài sau khi bỏ ô khuyết.
So sánh hai cách
Cách 1 mở rõ giải mã và tách hàng, phù hợp với mẫu JSON đã bảo đảm hợp lệ. Cách 2 trả cả hai dạng dữ liệu và có nhánh xử lý lỗi, nhưng quy tắc quy lỗi thành danh sách rỗng cần được chấp nhận rõ. Trong cả hai cách, không được dùng split(",") để thay bộ giải mã JSON.
Bài 12: Tính tỷ lệ chỗ ở có tiện nghi và bảo toàn mẫu số phân tích
Thực hiện tính toán tỷ lệ chỗ ở cung cấp tiện nghi Wifi trên toàn bộ tập dữ liệu, bảo đảm giữ đúng mẫu số tổng thể bao gồm cả những chỗ ở có danh sách tiện nghi rỗng.
Yêu cầu kỹ thuật:
- Tình huống: Chỗ ở có danh sách tiện nghi rỗng
'[]'không liệt kê tiện nghi nào trong dữ liệu. Khi tính tỷ lệ chỗ ở có Wifi, mẫu số bắt buộc phải là toàn bộ số lượng chỗ ở trong bảng, không được lấy mẫu số là số dòng sau khi explode hay loại bỏ các chỗ ở có danh sách rỗng. - Thao tác: Kiểm tra từng chỗ ở có chứa phần tử
"Wifi"trong danh sách hay không để tạo cờ booleanco_wifi, sau đó tính tỷ lệ bằng tổng số chỗ ở có Wifi chia cho tổng số dòng của bảng dữ liệu. - Đầu ra: Trả về từ điển gồm các khóa:
co_tien_nghi(Series boolean per listing),so_cho_o_co(int),tong_cho_o(int) vàty_le(float). - Trường hợp biên: Chỗ ở có mục
"Wifi, free"không được tính gộp vào mục"Wifi"nếu quy tắc yêu cầu khớp chính xác tên tiện nghi.
Lời giải
Cách 1: Tạo cờ một lần cho mỗi chỗ ở rồi tính tỷ lệ
python
# Dữ liệu mẫu và các hàm hỗ trợ
import json
import pandas as pd
DEMO_LISTINGS = pd.DataFrame(
{
"amenities": [
'["Wifi", "Air conditioning"]',
'["Wifi, free", "Kitchen"]',
"[]",
'["Kitchen"]',
'["Heating"]',
]
},
index=[101, 102, 103, 104, 105],
)
def parse_amenities_explode(
df: pd.DataFrame, col: str = "amenities"
) -> tuple[pd.Series, pd.Series]:
"""Giải mã chuỗi JSON và phân rã các mục tiện nghi thành từng dòng độc lập.
Tham số df: DataFrame chứa dữ liệu chỗ ở.
Tham số col: Tên cột chứa chuỗi JSON mảng tiện nghi.
Trả về: Tuple gồm Series danh sách Python ds và Series các mục tiện nghi muc.
"""
def safe_loads(val):
if pd.isna(val):
return []
try:
return json.loads(val)
except Exception:
return []
ds = df[col].map(safe_loads)
muc = ds.explode().dropna()
return ds, muc
ds_demo, muc_demo = parse_amenities_explode(DEMO_LISTINGS)
# Bắt đầu lời giải
co_wifi_c1 = ds_demo.map(lambda lst: "Wifi" in lst)
so_co_wifi_c1 = int(co_wifi_c1.sum())
tong_so_c1 = int(len(DEMO_LISTINGS))
ty_le_c1 = float(so_co_wifi_c1 / tong_so_c1)
print(f"Số chỗ ở có Wifi: {so_co_wifi_c1}/{tong_so_c1}")
print(f"Tỷ lệ chỗ ở có Wifi: {ty_le_c1:.1%}")
# Kết thúc lời giảiGiải thích Cách 1
ds_demo là Series danh sách còn giữ một dòng cho mỗi chỗ ở. lambda lst: "Wifi" in lst hỏi danh sách có phần tử bằng đúng chuỗi Wifi hay không. Hai tên Wifi và Wifi, free khác nhau, nên chỗ ở 102 không được tính theo quy tắc này.
.sum() trên cờ boolean đếm số chỗ ở có tiện nghi. len(DEMO_LISTINGS) lấy mẫu số từ bảng chỗ ở gốc, gồm cả dòng có danh sách rỗng. Trong năm dòng, chỉ chỗ ở 101 có đúng mục Wifi, nên tỷ lệ là 1 / 5, bằng 0.2.
Không lấy len(muc_demo) làm mẫu số vì mỗi dòng ở bảng dài là một lượt liệt kê tiện nghi, khác đơn vị chỗ ở. Chỗ ở có nhiều tiện nghi sẽ đóng góp nhiều dòng trong bảng dài, còn chỗ ở có danh sách rỗng biến mất sau bước bỏ ô khuyết.
Output Cách 1
| chỗ ở | Có đúng mục Wifi |
|---|---|
| 101 | True |
| 102 | False |
| 103 | False |
| 104 | False |
| 105 | False |
Ví dụ in Số chỗ ở có Wifi: 1/5 và Tỷ lệ chỗ ở có Wifi: 20.0%.
Cách 2: Đóng gói quy tắc tiện nghi mục tiêu và mẫu số
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import json
import pandas as pd
DEMO_LISTINGS = pd.DataFrame(
{
"amenities": [
'["Wifi", "Air conditioning"]',
'["Wifi, free", "Kitchen"]',
"[]",
'["Kitchen"]',
'["Heating"]',
]
},
index=[101, 102, 103, 104, 105],
)
def parse_amenities_explode(
df: pd.DataFrame, col: str = "amenities"
) -> tuple[pd.Series, pd.Series]:
"""Giải mã chuỗi JSON và phân rã các mục tiện nghi thành từng dòng độc lập.
Tham số df: DataFrame chứa dữ liệu chỗ ở.
Tham số col: Tên cột chứa chuỗi JSON mảng tiện nghi.
Trả về: Tuple gồm Series danh sách Python ds và Series các mục tiện nghi muc.
"""
def safe_loads(val):
if pd.isna(val):
return []
try:
return json.loads(val)
except Exception:
return []
ds = df[col].map(safe_loads)
muc = ds.explode().dropna()
return ds, muc
ds_demo, muc_demo = parse_amenities_explode(DEMO_LISTINGS)
# Bắt đầu lời giải
def calculate_amenity_rate(
df: pd.DataFrame, target_amenity: str = "Wifi", col: str = "amenities"
) -> dict:
"""Tính tỷ lệ chỗ ở có tiện nghi chỉ định trên toàn bộ mẫu số khảo sát.
Tham số df: DataFrame dữ liệu chỗ ở.
Tham số target_amenity: Tên tiện nghi cần kiểm tra sự hiện diện.
Tham số col: Tên cột chứa dữ liệu JSON tiện nghi.
Trả về: Từ điển chứa cờ boolean và các số liệu tỷ lệ tổng thể.
"""
ds, muc = parse_amenities_explode(df, col=col)
co_tien_nghi = ds.map(lambda lst: target_amenity in lst)
so_cho_o_co = int(co_tien_nghi.sum())
tong_cho_o = int(len(df))
ty_le = float(so_cho_o_co / tong_cho_o) if tong_cho_o > 0 else 0.0
return {
"co_tien_nghi": co_tien_nghi,
"so_cho_o_co": so_cho_o_co,
"tong_cho_o": tong_cho_o,
"ty_le": ty_le,
}
ket_qua_ty_le_wifi = calculate_amenity_rate(DEMO_LISTINGS, "Wifi")
print(f"Số chỗ ở có Wifi: {ket_qua_ty_le_wifi['so_cho_o_co']}")
print(f"Tổng số chỗ ở trong mẫu: {ket_qua_ty_le_wifi['tong_cho_o']}")
print(f"Tỷ lệ chính xác: {ket_qua_ty_le_wifi['ty_le']:.1%}")
display(ket_qua_ty_le_wifi["co_tien_nghi"].to_frame(name="co_wifi"))
# Kết thúc lời giảiGiải thích Cách 2
target_amenity cho phép thay tên cần kiểm tra, còn col chọn cột JSON. Hàm dùng parse_amenities_explode để có Series danh sách ds, sau đó kiểm tra phần tử trên từng danh sách. Biến muc được trả từ hàm hỗ trợ không được dùng làm mẫu số.
so_cho_o_co, tong_cho_o và ty_le tách rõ số đạt, tổng số và phép chia. co_tien_nghi giữ cờ cùng để người gọi kiểm tra từng chỗ ở. Với bảng rỗng, mã quy ước tỷ lệ bằng 0.0 để tránh chia cho 0. Trong báo cáo, cần chỉ rõ không có mẫu quan sát thay vì diễn giải giá trị dự phòng ấy như một tỷ lệ ước lượng.
Bài này dùng năm chuỗi JSON đã biết, bao gồm một danh sách rỗng. Nếu có dữ liệu khuyết hoặc JSON lỗi bị hàm hỗ trợ quy thành rỗng, tỷ lệ có thể bị diễn giải sai là đã biết chắc không có tiện nghi. Bài tiếp theo tách riêng trạng thái chưa có thông tin để chọn mẫu số phù hợp.
Output Cách 2
| chỗ ở | Có đúng mục Wifi |
|---|---|
| 101 | True |
| 102 | False |
| 103 | False |
| 104 | False |
| 105 | False |
Từ điển trả so_cho_o_co = 1, tong_cho_o = 5, ty_le = 0.2. Định dạng phần trăm .1% hiển thị 20.0%.
So sánh hai cách
Hai cách cùng đếm sự hiện diện nhị phân trên mỗi chỗ ở. Cách 2 giúp đổi tiện nghi và lấy đầy đủ thông tin kiểm tra trong một lần gọi. Lợi ích chính là làm rõ giao diện và mẫu số, không phải biến số lượt liệt kê tiện nghi thành số chỗ ở.
Bài 13: Phân biệt danh sách rỗng và dữ liệu khuyết thiếu khi tính tỷ lệ
Phân biệt bản chất thống kê giữa trạng thái danh sách tiện nghi rỗng [] (không có tiện nghi nào được liệt kê) và giá trị khuyết thiếu None (chưa thu thập được dữ liệu), từ đó tính toán tỷ lệ trên nhóm đã có thông tin.
Yêu cầu kỹ thuật:
- Tình huống: Xét một bảng nhỏ gồm 5 chỗ ở: P1 có danh sách
["Wifi", "Wifi", "Kitchen"], P2 có["Kitchen"], P3 có danh sách rỗng[], P4 nhận giá trịNonevà P5 có["Wifi"]. - Thao tác: Xác định số chỗ ở đã có thông tin
so_da_biet(các dòng có dữ liệu khácNone/NaN), sau đó tính tỷ lệ chỗ ở có Wifity_le_da_biettrên nhóm này. Lưu ý rằng P1 liệt kê lặp hai lần"Wifi"nhưng chỉ tính là một chỗ ở có tiện nghi. - Đầu ra: Trả về từ điển gồm:
so_da_biet(int),ty_le_da_biet(float) vàso_khuyet(int). - Trường hợp biên: Danh sách rỗng
[]được tính vàoso_da_biet, trong khiNonebị loại khỏi mẫu số của nhóm đã biết.
Lời giải
Cách 1: Lọc các danh sách không khuyết rồi kiểm tra Wifi
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_DS_NHO = pd.Series(
[
["Wifi", "Wifi", "Kitchen"],
["Kitchen"],
[],
None,
["Wifi"],
],
index=["P1", "P2", "P3", "P4", "P5"],
)
# Bắt đầu lời giải
da_biet_c1 = DEMO_DS_NHO.notna()
so_da_biet_c1 = int(da_biet_c1.sum())
ds_hop_le_c1 = DEMO_DS_NHO[da_biet_c1]
co_wifi_tap_con = ds_hop_le_c1.map(
lambda x: "Wifi" in x if isinstance(x, list) else False
)
so_co_wifi_c1 = int(co_wifi_tap_con.sum())
ty_le_tap_con_c1 = float(so_co_wifi_c1 / so_da_biet_c1)
print(f"Số chỗ ở đã biết thông tin: {so_da_biet_c1}")
print(f"Số chỗ ở có Wifi trong nhóm đã biết: {so_co_wifi_c1}")
print(f"Tỷ lệ trên nhóm đã biết: {ty_le_tap_con_c1:.1%}")
# Kết thúc lời giảiGiải thích Cách 1
.notna() trả True cho danh sách rỗng [] vì đó là một giá trị đã được cung cấp, và trả False cho ô None. Vì vậy bốn dòng P1, P2, P3, P5 thuộc nhóm đã biết. P4 được giữ ở trạng thái chưa biết, không tự kết luận có hoặc không có Wifi.
Phép kiểm tra "Wifi" in x chỉ trả một cờ, kể cả danh sách P1 chứa Wifi hai lần. Tổng cờ đúng là hai chỗ ở P1 và P5. Mẫu số đã biết là bốn, nên tỷ lệ bằng 0.5, không phải tỷ lệ trong toàn bộ năm chỗ ở.
isinstance(x, list) trong hàm ẩn danh kiểm tra kiểu trước khi thử thành viên. Theo miền dữ liệu của bài, ô không khuyết là danh sách. Nếu dữ liệu thật có giá trị khác kiểu nhưng không khuyết, cần báo lỗi hoặc tách cờ riêng, thay vì tự coi đó là bằng chứng không có tiện nghi.
Output Cách 1
| Đã biết danh sách | Có Wifi trong nhóm đã biết | |
|---|---|---|
| P1 | True | True |
| P2 | True | False |
| P3 | True | False |
| P4 | False | Chưa biết |
| P5 | True | True |
Kết quả in có so_da_biet = 4, số có Wifi trong nhóm là 2, tỷ lệ 50.0%.
Cách 2: Trả đồng thời số đã biết, số khuyết và tỷ lệ
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
DEMO_DS_NHO = pd.Series(
[
["Wifi", "Wifi", "Kitchen"],
["Kitchen"],
[],
None,
["Wifi"],
],
index=["P1", "P2", "P3", "P4", "P5"],
)
# Bắt đầu lời giải
def analyze_amenity_completeness(ds: pd.Series, target: str = "Wifi") -> dict:
"""Phân biệt danh sách rỗng với dữ liệu khuyết thiếu và tính tỷ lệ tiện nghi.
Tham số ds: Series chứa danh sách các tiện nghi của từng chỗ ở.
Tham số target: Tên tiện nghi cần kiểm tra.
Trả về: Từ điển chứa số lượng đã biết, số khuyết và tỷ lệ tiện nghi tương ứng.
"""
so_da_biet = int(ds.notna().sum())
so_khuyet = int(ds.isna().sum())
ds_da_biet = ds.dropna()
co_target = ds_da_biet.map(
lambda lst: target in lst if isinstance(lst, list) else False
)
ty_le_da_biet = float(co_target.sum() / so_da_biet) if so_da_biet > 0 else 0.0
return {
"so_da_biet": so_da_biet,
"so_khuyet": so_khuyet,
"ty_le_da_biet": ty_le_da_biet,
}
ket_qua_day_du = analyze_amenity_completeness(DEMO_DS_NHO, target="Wifi")
print("Kết quả phân tích từ analyze_amenity_completeness:")
print(f"Số chỗ ở đã biết thông tin: {ket_qua_day_du['so_da_biet']}")
print(f"Số chỗ ở khuyết thiếu dữ liệu: {ket_qua_day_du['so_khuyet']}")
print(f"Tỷ lệ có Wifi trên nhóm đã biết: {ket_qua_day_du['ty_le_da_biet']:.1%}")
# Kết thúc lời giảiGiải thích Cách 2
Hàm tính .notna().sum() và .isna().sum() trên cùng Series để tách bốn dòng có dữ liệu khỏi một dòng khuyết. .dropna() tạo tập dùng để hỏi thành viên, nhờ vậy phép in không bị áp vào None.
co_target.sum() / so_da_biet dùng đúng tập đã biết làm mẫu số. Phần tử trùng trong một danh sách không tăng tử số vì mỗi dòng chỉ cho một cờ boolean. Nếu không có dòng đã biết, nhánh dự phòng trả 0.0. Đó là lựa chọn giao diện của mã, không phải kết luận xác suất khi chưa có thông tin.
Từ điển không trả tỷ lệ có Wifi của toàn bộ quần thể năm dòng vì chưa biết P4. Thay P4 bằng danh sách rỗng sẽ là bổ sung một giả thiết về dữ liệu, chứ không phải chỉ làm sạch kiểu.
Output Cách 2
| Khóa trả về | Output |
|---|---|
so_da_biet | 4 |
so_khuyet | 1 |
ty_le_da_biet | 0.5 |
So sánh hai cách
Cách 1 cho thấy rõ mặt nạ và tập con được chọn. Cách 2 gói các chỉ số bao phủ cùng tỷ lệ để khó bỏ sót lượng dữ liệu khuyết khi báo cáo. Cả hai giữ phân biệt danh sách rỗng với chưa có danh sách, và chỉ đếm một lần cho mỗi chỗ ở có tiện nghi.
Bài 14: Kiểm chứng tín hiệu từ khoá đa ngữ trên các nhóm đánh giá
Xây dựng hai tín hiệu từ khoá đa ngữ đại diện cho mạng Internet (wifi|internet) và bãi đỗ xe (parking|estacionamiento), tính toán tỷ lệ xuất hiện của từng tín hiệu trên hai phân nhóm ngôn ngữ và kiểm chứng phản biện giả thuyết về sự hài lòng của khách hàng.
Yêu cầu kỹ thuật:
- Tình huống: Người ta thường đặt giả thuyết rằng "đếm số đánh giá chứa từ khoá wifi là đếm số khách hàng hài lòng về mạng". Cần chứng minh tính phiến diện của nhận định này bằng cách chỉ ra các đánh giá phàn nàn dịch vụ mạng yếu cũng chứa từ khoá đó.
- Thao tác: Áp dụng phương thức
.str.contains()không phân biệt hoa thường với hai mẫu regex trên hai tập con ngôn ngữ, tính tỷ lệ phần trăm theo từng nhóm. - Đầu ra: Trả về đối chiếu gồm 3 cột:
tin_hieu,ty_le_nhom_esvàty_le_nhom_khac. - Trường hợp biên: Xử lý an toàn khi một trong hai nhóm ngôn ngữ không có quan sát nào.
Lời giải
Cách 1: Tạo các cờ từ khóa rồi tính theo từng nhóm
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
def language_flag(comments: pd.Series, pattern: str = PAT_ES) -> dict:
"""Tạo cờ nhận diện ngôn ngữ theo quy tắc từ khoá và tính tỷ lệ bao phủ.
Tham số comments: Series chuỗi cần phân loại.
Tham số pattern: Biểu thức chính quy chứa các từ khoá đặc trưng.
Trả về: Từ điển chứa cờ la_es kiểu bool và tỷ lệ ty_le_es.
"""
la_es = comments.str.contains(pattern, case=False, na=False)
ty_le_es = float(la_es.sum() / len(comments)) if len(comments) > 0 else 0.0
return {"la_es": la_es, "ty_le_es": ty_le_es}
ket_qua_es = language_flag(c_sach_demo)
flag_es = ket_qua_es["la_es"]
flag_es_demo = flag_es.copy()
# Bắt đầu lời giải
pat_wifi = r"wifi|internet"
pat_park = r"parking|estacionamiento"
flag_es_demo = ket_qua_es["la_es"]
c_demo = c_sach_demo
co_w = c_demo.str.contains(pat_wifi, case=False, na=False)
co_p = c_demo.str.contains(pat_park, case=False, na=False)
c_es = c_demo[flag_es_demo]
c_khac = c_demo[~flag_es_demo]
ty_le_w_es = co_w[flag_es_demo].sum() / len(c_es) if len(c_es) > 0 else 0.0
ty_le_w_khac = co_w[~flag_es_demo].sum() / len(c_khac) if len(c_khac) > 0 else 0.0
print(f"Tỷ lệ nhắc Wifi ở nhóm es: {ty_le_w_es:.1%}")
print(f"Tỷ lệ nhắc Wifi ở nhóm khác: {ty_le_w_khac:.1%}")
ty_le_p_es = co_p[flag_es_demo].mean() if len(c_es) > 0 else 0.0
ty_le_p_khac = co_p[~flag_es_demo].mean() if len(c_khac) > 0 else 0.0
bang_tin_hieu = pd.DataFrame(
{
"tin_hieu": ["wifi_internet", "parking_estacionamiento"],
"ty_le_nhom_es": [round(float(ty_le_w_es), 3), round(float(ty_le_p_es), 3)],
"ty_le_nhom_khac": [
round(float(ty_le_w_khac), 3),
round(float(ty_le_p_khac), 3),
],
}
)
display(bang_tin_hieu)
# Kết thúc lời giảiGiải thích Cách 1
Dấu | trong wifi|internet và parking|estacionamiento nối các lựa chọn tìm kiếm. case=False cho phép nhận WiFi, còn na=False xác định cờ không khớp cho ô thiếu. Mỗi dòng chỉ đóng góp một cờ cho mỗi tín hiệu, kể cả khi nhắc từ khóa nhiều lần.
flag_es_demo chọn bảy dòng khớp quy tắc ngôn ngữ của Bài 7, ~flag_es_demo chọn tám dòng còn lại. Cờ từ khóa được lọc bằng cùng mặt nạ, rồi tổng cờ chia cho số dòng của từng nhóm. Các điều kiện độ dài nhóm giúp tránh chia cho 0. Mẫu đang dùng đã bỏ ô khuyết trước khi tạo các nhóm.
Chỉ một dòng không khớp quy tắc ngôn ngữ có từ khóa Wifi, cho tỷ lệ 1 / 8 bằng 12.5%. Không có dòng nào nhắc các từ khóa bãi đỗ xe. Đây là các cờ khớp quy tắc trên fixture, không phải hai nhóm ngôn ngữ được xác nhận hoặc bằng chứng về sự hài lòng.
Output Cách 1
tin_hieu | ty_le_nhom_es | ty_le_nhom_khac |
|---|---|---|
wifi_internet | 0.0 | 0.125 |
parking_estacionamiento | 0.0 | 0.0 |
Các lệnh in Wifi trong đoạn minh họa trả 0.0% cho nhóm cờ đúng và 12.5% cho nhóm cờ sai. Cờ co_p của bãi đỗ xe đều False trong mẫu này.
Cách 2: Đưa cả hai tín hiệu vào một bảng kết quả
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
DEMO_COMMENTS = pd.Series(
[
"Excelente ubicación, muy cerca del metro.",
"Great place near the metro, very clean.",
"Todo perfecto.<br/>Volvería sin duda.",
".",
None,
"El departamento está cerca de todo, muy cómodo.",
"Nice host.<br/>Near Movistar Arena!<br/>Recommended.",
"Ok",
"Muy buena estadía cerca de la estación Baquedano.",
"Lovely apartment, close to everything.",
"Departamento limpio y cerca de muchos restaurantes.",
"The departamento was great, near the mall.",
"Atención excelente, cerca del centro.",
"WiFi rápido, near the metro station.",
"",
"Buena comunicación con el anfitrión.<br/>Recomendado cerca de Movistar Arena.",
],
name="comments",
)
c_sach_demo = DEMO_COMMENTS.str.replace("<br/>", " ", regex=False)
c_sach_full = c_sach_demo.copy()
PAT_ES = r"ción|ñ|muy|excelente|departamento"
def language_flag(comments: pd.Series, pattern: str = PAT_ES) -> dict:
"""Tạo cờ nhận diện ngôn ngữ theo quy tắc từ khoá và tính tỷ lệ bao phủ.
Tham số comments: Series chuỗi cần phân loại.
Tham số pattern: Biểu thức chính quy chứa các từ khoá đặc trưng.
Trả về: Từ điển chứa cờ la_es kiểu bool và tỷ lệ ty_le_es.
"""
la_es = comments.str.contains(pattern, case=False, na=False)
ty_le_es = float(la_es.sum() / len(comments)) if len(comments) > 0 else 0.0
return {"la_es": la_es, "ty_le_es": ty_le_es}
ket_qua_es = language_flag(c_sach_demo)
flag_es = ket_qua_es["la_es"]
flag_es_demo = flag_es.copy()
# Bắt đầu lời giải
def multilingual_keyword_signals(
comments: pd.Series, flag_es: pd.Series
) -> pd.DataFrame:
"""Phân tích tỷ lệ xuất hiện các tín hiệu từ khoá đa ngữ trên hai phân nhóm.
Tham số comments: Series văn bản đã làm sạch.
Tham số flag_es: Series kiểu bool phân loại nhóm ngôn ngữ tiếng Tây Ban Nha.
Trả về: DataFrame so sánh tỷ lệ nhắc đến từng tín hiệu giữa hai nhóm.
"""
pat_wifi = r"wifi|internet"
pat_parking = r"parking|estacionamiento"
co_wifi = comments.str.contains(pat_wifi, case=False, na=False)
co_parking = comments.str.contains(pat_parking, case=False, na=False)
es_comments = comments[flag_es]
khac_comments = comments[~flag_es]
ty_le_wifi_es = (
float(co_wifi[flag_es].sum() / len(es_comments))
if len(es_comments) > 0
else 0.0
)
ty_le_wifi_khac = (
float(co_wifi[~flag_es].sum() / len(khac_comments))
if len(khac_comments) > 0
else 0.0
)
ty_le_park_es = (
float(co_parking[flag_es].sum() / len(es_comments))
if len(es_comments) > 0
else 0.0
)
ty_le_park_khac = (
float(co_parking[~flag_es].sum() / len(khac_comments))
if len(khac_comments) > 0
else 0.0
)
return pd.DataFrame(
{
"tin_hieu": ["wifi_internet", "parking_estacionamiento"],
"ty_le_nhom_es": [round(ty_le_wifi_es, 3), round(ty_le_park_es, 3)],
"ty_le_nhom_khac": [round(ty_le_wifi_khac, 3), round(ty_le_park_khac, 3)],
}
)
df_tin_hieu = multilingual_keyword_signals(c_sach_demo, flag_es_demo)
display(df_tin_hieu)
# Kết thúc lời giảiGiải thích Cách 2
multilingual_keyword_signals tính hai cờ riêng và bốn tỷ lệ theo cặp tín hiệu, nhóm cờ. Các cột tỷ lệ giữ dạng số từ 0 đến 1. .round(..., 3) trong lúc dựng bảng chỉ giới hạn số chữ số báo cáo, nên 0.125 vẫn tương ứng 12.5%.
Series flag_es phải có phù hợp với comments để mặt nạ chọn đúng dòng. Hàm dựa vào các nhóm đã có, không nhận diện lại ngôn ngữ. Nhánh nhóm rỗng trả tỷ lệ 0.0 theo quy ước mã, nên khi dùng kết quả ngoài bài mẫu cần báo thêm số dòng mỗi nhóm.
Các câu WiFi rất chậm và el wifi no funcionaba vẫn chứa từ khóa dù đang phàn nàn. Vì vậy chỉ số đo việc nhắc đến từ hoặc đoạn chữ, không đo hướng cảm xúc. Kết quả bằng 0 cũng chỉ nói không tìm thấy mẫu chữ đã quy định, chưa chứng minh tiện nghi không tồn tại hoặc người viết không quan tâm.
Output Cách 2
tin_hieu | ty_le_nhom_es | ty_le_nhom_khac |
|---|---|---|
wifi_internet | 0.0 | 0.125 |
parking_estacionamiento | 0.0 | 0.0 |
So sánh hai cách
Cách 1 mở rõ mặt nạ của từng tín hiệu và mẫu số từng nhóm. Cách 2 trả đồng thời hai tín hiệu đúng cấu trúc bảng theo đề. Trước khi diễn giải chênh lệch giữa nhóm, cần xem số dòng, kiểm tra lỗi phân nhóm và đọc ngữ cảnh, thay vì suy ra quốc tịch, mức quan tâm hoặc hài lòng từ cờ từ khóa.
Bài 15: Trích xuất số đo diện tích từ chuỗi văn bản tự do với nhiều quy chuẩn viết
Xây dựng biểu thức chính quy để trích xuất số đo diện tích phòng (đơn vị m²) từ trường văn bản tự do, hỗ trợ các cách viết thông dụng như 45 m2, 30m², 45.5 m2, 45,5 m² và loại trừ các chuỗi mã không hợp lệ như 30m2x.
Yêu cầu kỹ thuật:
- Tình huống: Người cho thuê mô tả diện tích phòng bằng nhiều quy chuẩn khác nhau: Dùng ký tự số 2 (
m2) hoặc ký tự số mũ (m²), có hoặc không có dấu cách, dùng dấu chấm hoặc dấu phẩy phân cách phần thập phân. Tuy nhiên, các chuỗi mã hàng như"30m2x"không được coi là diện tích. - Thao tác: Dùng biểu thức chính quy
r"(?i)\b(\d+(?:[.,]\d+)?)\s*(?:m2|m²)\b", chuẩn hoá dấu phẩy thập phân thành dấu chấm, rồi ép kiểu sang số thựcfloat. - Đầu ra: Trả về Series số thực biểu diễn diện tích phòng, gán
NaNcho các dòng không chứa thông tin diện tích hợp lệ. - Trường hợp biên: Dùng biên từ
\bđể ngăn ngừa việc khớp một phần trong các từ dài hơn, chuẩn hoá dấu phẩy trước khi gọipd.to_numeric().
Lời giải
Cách 1: Trích phần số rồi chuẩn hóa dấu thập phân
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
# Bắt đầu lời giải
mau_phong = pd.Series(
[
"Căn hộ 45 m2 thoáng mát",
"Phòng 30m² trung tâm",
"Studio 45.5 m2 sạch sẽ",
"Chỗ ở 45,5 m² đẹp",
"Mã phòng 30m2x không nhận",
"Không ghi diện tích",
]
)
pat_dien_tich = r"(?i)\b(\d+(?:[.,]\d+)?)\s*(?:m2|m²)\b"
so_trich_raw = mau_phong.str.extract(pat_dien_tich, expand=False)
dien_tich_c1 = pd.to_numeric(
so_trich_raw.str.replace(",", ".", regex=False), errors="coerce"
)
df_dien_tich_c1 = pd.DataFrame({"mo_ta": mau_phong, "dien_tich_m2": dien_tich_c1})
display(df_dien_tich_c1)
# Kết thúc lời giảiGiải thích Cách 1
Cờ (?i) đặt quy tắc không phân biệt hoa thường. Nhóm bắt (\d+(?:[.,]\d+)?) lấy một dãy chữ số và phần thập phân tùy chọn viết bằng dấu chấm hoặc dấu phẩy. Chỉ phần số được trả về, vì phần đơn vị (?:m2|m²) là nhóm không bắt.
\s* nhận không hoặc nhiều ký tự khoảng trắng, nên 45 m2 và 30m² đều khớp. Biên từ \b sau đơn vị chặn mẫu 30m2x, vì ký tự x vẫn tiếp tục cùng một từ. Các biên này giúp xử lý các ca trong đề, không thay thế việc kiểm tra mọi cú pháp số có thể gặp.
str.extract(..., expand=False) trả phần số của lần khớp đầu tiên trong mỗi dòng. .str.replace(",", ".", regex=False) đưa dấu thập phân về dấu chấm. pd.to_numeric(..., errors="coerce") chuyển phần số thành dữ liệu tính toán được và giữ dòng không khớp ở trạng thái khuyết.
Output Cách 1
| Vị trí | Mô tả | dien_tich_m2 |
|---|---|---|
| 0 | Căn hộ 45 m2 thoáng mát | 45.0 |
| 1 | Phòng 30m² trung tâm | 30.0 |
| 2 | Studio 45.5 m2 sạch sẽ | 45.5 |
| 3 | Chỗ ở 45,5 m² đẹp | 45.5 |
| 4 | Mã phòng 30m2x không nhận | NaN |
| 5 | Không ghi diện tích | NaN |
Cách 2: Gói trích diện tích thành hàm trả Series số
python
# Dữ liệu mẫu và các hàm hỗ trợ
from IPython.display import display
import pandas as pd
mau_phong = pd.Series(
[
"Căn hộ 45 m2 thoáng mát",
"Phòng 30m² trung tâm",
"Studio 45.5 m2 sạch sẽ",
"Chỗ ở 45,5 m² đẹp",
"Mã phòng 30m2x không nhận",
"Không ghi diện tích",
]
)
# Bắt đầu lời giải
def extract_area(s: pd.Series) -> pd.Series:
"""Trích xuất giá trị diện tích m2 từ chuỗi mô tả tự do và đổi sang số thực.
Tham số s: Series chuỗi mô tả phòng.
Trả về: Series số thực kiểu float biểu diễn diện tích mét vuông.
"""
pat = r"(?i)\b(\d+(?:[.,]\d+)?)\s*(?:m2|m²)\b"
raw = s.str.extract(pat, expand=False)
clean_num = raw.str.replace(",", ".", regex=False)
return pd.to_numeric(clean_num, errors="coerce")
ket_qua_dien_tich = extract_area(mau_phong)
display(pd.DataFrame({"mo_ta": mau_phong, "dien_tich_m2": ket_qua_dien_tich}))
# Kết thúc lời giảiGiải thích Cách 2
extract_area đặt mẫu, kết quả trích raw và chuỗi số đã chuẩn hóa clean_num trong cùng một hàm. Kết quả giữ của cột mô tả, nên có thể ghép trở lại đúng dòng chỗ ở trong .
Chuyển dấu phẩy ở đây chỉ có nghĩa dấu phẩy thập phân theo miền của bài. Nếu dữ liệu còn dùng dấu phẩy để nhóm hàng nghìn, quy tắc cần được làm rõ trước khi thay thế. Tương tự, nếu một câu có nhiều diện tích, extract chỉ lấy lần khớp đầu, chưa biết đó là diện tích phòng, ban công hay đất.
Biểu thức chưa quy định số âm, khoảng giá trị, ký hiệu khoa học hoặc mọi ràng buộc số viết sai. Nó có thể khớp một phần của những dạng ngoài miền đã mô tả. Khi mở rộng, cần thêm ca kiểm thử và kiểm tra miền giá trị, không chỉ thêm nhiều nhánh regex rồi mặc định mọi đầu ra đều đúng về ngữ nghĩa.
Output Cách 2
| Vị trí | Mô tả | dien_tich_m2 |
|---|---|---|
| 0 | Căn hộ 45 m2 thoáng mát | 45.0 |
| 1 | Phòng 30m² trung tâm | 30.0 |
| 2 | Studio 45.5 m2 sạch sẽ | 45.5 |
| 3 | Chỗ ở 45,5 m² đẹp | 45.5 |
| 4 | Mã phòng 30m2x không nhận | NaN |
| 5 | Không ghi diện tích | NaN |
So sánh hai cách
Hai cách cùng trích được bốn số và để hai dòng còn lại khuyết. Cách 1 mở rõ từng bước để quan sát chuỗi trung gian. Cách 2 tạo một thao tác dùng lại cho cột mô tả. Cả hai chỉ xác nhận các dạng diện tích đã quy định, không chứng minh đơn vị và con số mô tả đúng thực tế.
Bonus regex: 20 bài tập kiểm tra và trích xuất chuỗi
Một mã sản phẩm, số điện thoại hay địa chỉ tệp đều có quy tắc về ký tự và thứ tự. Trong 20 bài dưới đây, hãy viết rõ quy tắc đó bằng biểu thức chính quy, rồi kiểm tra cả chuỗi đúng lẫn chuỗi gần đúng.
Quy ước chung
Các bài dùng chung những quy ước sau:
- Kiểu dữ liệu đầu vào: Mỗi hàm nhận đầu vào là chuỗi
strhoặc giá trị khuyết thiếuNone. Không thực hiện các thao tác tiền xử lý làm biến dạng chuỗi gốc như tự ý cắt tỉa khoảng trắng (strip) hay hạ chữ thường (lower) trước khi kiểm tra định dạng. - Miền chữ số và ký tự: Toàn bộ chữ số được quy ước trên dải ký tự ASCII chuẩn
[0-9]. Biểu thức chính quy từ chối các ký tự số thuộc dải Unicode mở rộng (như chữ số toàn giác hoặc chữ số Ả Rập), trừ bài tập có yêu cầu xử lý khoảng trắng Unicode riêng biệt. - Phân biệt hoa thường: Mặc định mọi bài toán đều phân biệt chữ hoa và chữ thường với cờ quy ước bằng 0. Riêng Bài 10 cho phép sử dụng cờ
re.IGNORECASE(viết tắt làre.I) để nhận diện linh hoạt các biến thể viết tắt của phòng ngủ. - Giới hạn tính năng regex: Tuyệt đối không sử dụng các kỹ thuật nhìn trước/nhìn sau (lookaround assertions) hoặc tham chiếu ngược (backreferences). Giới hạn này giúp tập trung vào ký tự, lượng từ và nhóm bắt. Nó không có nghĩa bộ máy
recủa Python luôn xử lý mẫu bằng một thuật toán không quay lui. - Bảo toàn kiểu dữ liệu đầu ra: Phân biệt rành mạch giữa các bài toán kiểm tra tính hợp lệ (trả về giá trị boolean
TruehoặcFalse, trong đóNoneluôn trả vềFalse) và bài toán trích xuất thuộc tính (trả vềstr,tuplehoặcNone). Tuyệt đối không ép kiểu số nguyên đối với các mã số nhận diện nhằm tránh làm mất các chữ số 0 ở đầu. - Hai phương pháp giải độc lập: Mỗi bài toán đều cung cấp hai giải pháp hoàn chỉnh: Cách 1 dành cho người mới bắt đầu (gọi trực tiếp hàm từ module
re) và Cách 2 ứng dụng thực tế (biên dịch trước mẫu vớire.compile). Mọi khối mã đều độc lập, có đủ câu lệnh nạp thư viện, hàm xử lý, dữ liệu mẫu và lời gọi kiểm thử in kết quả.
Các hàm như re.search và re.fullmatch cũng lưu đệm các mẫu dùng gần đây. Vì vậy, re.compile giúp quản lý và tái sử dụng mẫu, nhưng không tự bảo đảm chương trình chạy nhanh hơn. Hai cách trong mỗi bài dùng cùng mẫu và phải trả về cùng kết quả. Tài liệu Python về re.compile.
Danh mục bài tập thực hành
Bài 1: Trích dãy chữ số đầu tiên trong chuỗi văn bản
Trong quá trình thu thập và làm sạch dữ liệu văn bản từ biểu mẫu nhập liệu hoặc nhật ký hệ thống, mã số định danh thường bị lẫn lộn giữa các ký tự chữ hoặc dấu câu. Bài toán đặt ra là cần lấy ra chuỗi chữ số ASCII xuất hiện đầu tiên để làm khóa tra cứu ban đầu.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy để tìm kiếm dãy chữ số ASCII liên tiếp đầu tiên mà không làm thay đổi các ký tự gốc.
- Đầu ra: Trả về chuỗi
strchứa các chữ số tìm thấy đầu tiên. NếutextlàNonehoặc không chứa chữ số ASCII nào, trả vềNone. Tuyệt đối không chuyển đổi sang số nguyênintnhằm tránh làm mất các chữ số 0 ở đầu (ví dụ: Chuỗi'007'). - Trường hợp biên: Giá trị
None, chuỗi rỗng'', chuỗi không có số, chữ số dạng Unicode toàn giác như'123'hoặc chữ số Ả Rập như'١٢٣'đều phải trả vềNone.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[0-9]+"
def solve(text):
if text is None:
return None
m = re.search(PATTERN, text, flags=0)
return m.group(0) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"abc123xyz",
'define "123"',
"var g = 123;",
"P007 / P12",
"không có mã",
None,
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[0-9]+":- Lớp ký tự
[0-9]: Chỉ so khớp đúng các ký tự chữ số thuộc bảng mã ASCII chuẩn từ0đến9. Lớp ký tự này từ chối các ký tự chữ số thuộc dải Unicode mở rộng như chữ số toàn giác'123'hay chữ số Ả Rập'١٢٣', giữ đúng miền chữ số của đề bài. - Lượng từ
+: Yêu cầu xuất hiện một hoặc nhiều lần liên tiếp, so khớp tham lam (greedy) toàn bộ dãy số liền kề.
- Lớp ký tự
- Phân tích hàm và giao thức gọi lệnh regex:
- Kiểm tra điều kiện đầu vào: Nếu
text is None, hàm lập tức trả vềNonemà không truyền vàore.searchnhằm tránh phát sinh ngoại lệTypeError. - Hàm
re.search(PATTERN, text, flags=0): Quét từ trái sang phải trên chuỗitextvà dừng lại ngay tại vị trí đầu tiên tìm thấy đoạn con khớp với mẫu. - Đối tượng
Matchvà phương thức.group(0): Nếu có kết quả khớp (m is not None),m.group(0)trả về toàn bộ chuỗi con vừa khớp. Nếu không tìm thấy,mmang giá trịNonevà hàm trả vềNone.
- Kiểm tra điều kiện đầu vào: Nếu
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'abc123xyz' | '123' | str |
'define "123"' | '123' | str |
'var g = 123;' | '123' | str |
'P007 / P12' | '007' | str |
'không có mã' | None | NoneType |
None | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[0-9]+")
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.search(text)
return m.group(0) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"abc123xyz",
'define "123"',
"var g = 123;",
"P007 / P12",
"không có mã",
None,
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.search(text): Thực hiện tìm kiếm trực tiếp trên chuỗi đầu vào mà không cần lặp lại bước kiểm tra cú pháp mẫu ở mỗi lời gọi.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'abc123xyz' | '123' | str |
'define "123"' | '123' | str |
'var g = 123;' | '123' | str |
'P007 / P12' | '007' | str |
'không có mã' | None | NoneType |
None | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.search: Thuận tiện cho các tác vụ phân tích nhanh, kịch bản thử nghiệm ngắn, hoặc khi mẫu biểu thức chính quy chỉ dùng một lần duy nhất. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 2: Dấu chấm thường và ký tự đại diện trong chuỗi ba ký tự
Trong việc xác thực mã định danh ngắn gọn từ biểu mẫu người dùng, hệ thống quy định mã hợp lệ phải gồm đúng ba ký tự bất kỳ và bắt buộc kết thúc bằng một dấu chấm thông thường. Cần kiểm tra toàn bộ chuỗi để từ chối các mã sai độ dài.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy kiểm tra toàn chuỗi (fullmatch), phân biệt giữa ký tự đại diện chấm và ký tự dấu chấm nguyên văn.
- Đầu ra: Trả về
Truenếu toàn bộ chuỗi thỏa mãn mẫu định dạng, ngược lại trả vềFalse. Đầu vàoNonebắt buộc trả vềFalse. - Trường hợp biên: Ba ký tự đầu có thể là chữ cái, chữ số, khoảng trắng hoặc dấu câu, nhưng không chứa ký tự xuống dòng
\n. Ký tự cuối cùng bắt buộc là dấu chấm literal. Chuỗi thiếu ký tự (như'ab.') hoặc thừa ký tự (như'abcd.') đều bị từ chối.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"...\."
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["cat.", "896.", "?=+.", "abc1", "ab.", "abcd."]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"...\.":- Ba dấu chấm đầu
...: Mỗi dấu chấm đóng vai trò là một ký tự đại diện (wildcard), so khớp với đúng một ký tự bất kỳ ngoại trừ ký tự xuống dòng\n(khi không bật cờre.DOTALL). Ba dấu chấm liên tiếp yêu cầu chính xác ba ký tự ở tiền tố. - Ký tự thoát
\.: Ký tự gạch chéo ngược vô hiệu hóa tính năng đại diện của dấu chấm, buộc vị trí thứ tư phải là một ký tự dấu chấm nguyên văn.
- Ba dấu chấm đầu
- Phân tích hàm và giao thức gọi lệnh regex:
- Kiểm tra điều kiện đầu vào: Nếu
text is None, hàm lập tức trả vềFalsetheo đúng quy ước kiểm tra logic boolean của đề bài. - Hàm
re.fullmatch(PATTERN, text, flags=0): Yêu cầu toàn bộ chuỗi từ vị trí bắt đầu đến vị trí kết thúc phải khớp trọn vẹn với mẫu. Điều này ngăn chặn việc khớp một phần khi chuỗi dài hơn bốn ký tự.
- Kiểm tra điều kiện đầu vào: Nếu
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'cat.' | True | bool |
'896.' | True | bool |
'?=+.' | True | bool |
'abc1' | False | bool |
'ab.' | False | bool |
'abcd.' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"...\.")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["cat.", "896.", "?=+.", "abc1", "ab.", "abcd."]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Kiểm tra độ dài và từng vị trí ký tự từ đầu đến cuối chuỗi một cách trực tiếp.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'cat.' | True | bool |
'896.' | True | bool |
'?=+.' | True | bool |
'abc1' | False | bool |
'ab.' | False | bool |
'abcd.' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rõ ràng, dễ đọc cho các đoạn mã kiểm tra tính hợp lệ nhanh trong các câu lệnh điều kiện đơn lẻ. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 3: Chọn ký tự trong tập cho trước kết hợp phần đuôi cố định
Trong việc chuẩn hóa các từ viết tắt hoặc danh từ quy ước trong kho dữ liệu, hệ thống chỉ chấp nhận các từ có ký tự đầu thuộc tập hợp c, m, f và hai ký tự tiếp theo bắt buộc là an. Mọi từ khác ngoài danh mục này đều coi là không hợp lệ.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng lớp ký tự (character class) trong biểu thức chính quy để kiểm tra toàn bộ chuỗi theo đúng tập ký tự cho phép.
- Đầu ra: Trả về
Truenếu chuỗi khớp chính xác một trong các từcan,man,fan. Ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Phân biệt hoa thường, từ chối chữ hoa như
'Can'hay'FAN'. Khoảng trắng ở đầu hoặc cuối (như' can ') làm chuỗi không hợp lệ vì phương thứcfullmatchkiểm tra toàn bộ chuỗi.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[cmf]an"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["can", "man", "fan", "dan", "ran", "pan"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[cmf]an":- Lớp ký tự
[cmf]: Định nghĩa một tập hợp gồm đúng ba ký tự chữ cái viết thường. Biểu thức chỉ chấp nhận duy nhất một ký tự tại vị trí này nếu ký tự đó làc,mhoặcf. - Hai ký tự cố định
an: Đòi hỏi hai ký tự tiếp theo phải xuất hiện nguyên vẹn theo đúng thứ tự.
- Lớp ký tự
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Buộc chuỗi phải có đúng ba ký tự. Nếu dùng dấu chấm đại diện.thay cho[cmf], mẫu sẽ nhận nhầm cả các từ ngoài danh mục nhưdan,ran,pan. Do đó, lớp ký tự đóng vai trò hạn định chính xác không gian ký tự hợp lệ.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'can' | True | bool |
'man' | True | bool |
'fan' | True | bool |
'dan' | False | bool |
'ran' | False | bool |
'pan' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[cmf]an")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["can", "man", "fan", "dan", "ran", "pan"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Kiểm tra toàn bộ chuỗi. Kết quả làMatchnếu khớp, hoặcNonenếu không khớp.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'can' | True | bool |
'man' | True | bool |
'fan' | True | bool |
'dan' | False | bool |
'ran' | False | bool |
'pan' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Gọn nhẹ, thích hợp khi kiểm tra dữ liệu trong các hàm phụ trợ dùng một lần. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 4: Kết hợp các dải khoảng ký tự rời rạc theo từng vị trí
Trong hệ thống mã hóa định danh kiểm kê, mỗi mã hợp lệ bắt buộc gồm đúng ba ký tự với quy chuẩn chặt chẽ theo từng vị trí: Vị trí đầu là chữ hoa từ A đến C, vị trí thứ hai là chữ thường từ n đến p, vị trí cuối cùng là chữ thường từ a đến c.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng dải ký tự (character range) với dấu gạch ngang bên trong ngoặc vuông để thiết lập điều kiện cho từng vị trí độc lập.
- Đầu ra: Trả về
Truenếu chuỗi thỏa mãn trọn vẹn cả ba khoảng ký tự, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Phân biệt hoa thường tuyệt đối. Bao gồm cả hai đầu mút của dải (chẳng hạn
'Cpc'hợp lệ vì C, p, c đều nằm tại cận trên tương ứng). Chuỗi có bất kỳ ký tự nào nằm ngoài dải quy định đều bị từ chối.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[A-C][n-p][a-c]"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["Ana", "Bob", "Cpc", "aax", "bby", "ccz"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[A-C][n-p][a-c]":- Vị trí thứ nhất
[A-C]: Đại diện cho một ký tự chữ cái viết hoa thuộc tập{A, B, C}. - Vị trí thứ hai
[n-p]: Đại diện cho một ký tự chữ cái viết thường thuộc tập{n, o, p}. - Vị trí thứ ba
[a-c]: Đại diện cho một ký tự chữ cái viết thường thuộc tập{a, b, c}.
- Vị trí thứ nhất
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Kiểm tra toàn diện toàn bộ chuỗi. Mẫu không chứa lượng từ lặp nào nên chuỗi hợp lệ bắt buộc phải có độ dài chính xác bằng ba ký tự. Bất kỳ sự lệch hoa thường hay vượt quá biên dải mã ASCII đều khiến hàm trả vềFalse.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'Ana' | True | bool |
'Bob' | True | bool |
'Cpc' | True | bool |
'aax' | False | bool |
'bby' | False | bool |
'ccz' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[A-C][n-p][a-c]")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["Ana", "Bob", "Cpc", "aax", "bby", "ccz"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét và kiểm định tính hợp lệ của chuỗi một cách trực tiếp mà không cần phân tích lại chuỗi biểu thức chính quy.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'Ana' | True | bool |
'Bob' | True | bool |
'Cpc' | True | bool |
'aax' | False | bool |
'bby' | False | bool |
'ccz' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Đơn giản, trực quan, phù hợp khi chỉ cần xác thực dữ liệu tại một vài vị trí mã nguồn riêng lẻ. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 5: Giới hạn số lần lặp với khoảng định lượng số lần xuất hiện
Trong việc phân tích dữ liệu văn bản từ mạng xã hội hoặc diễn đàn trao đổi, người dùng thường kéo dài một số ký tự để nhấn mạnh cảm xúc. Hệ thống yêu cầu xác định các từ bắt đầu bằng wa, theo sau bởi từ 3 đến 5 ký tự z, và kết thúc bằng up.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng cú pháp định lượng khoảng
{m,n}để kiểm soát chính xác số lần lặp tối thiểu và tối đa của ký tự mục tiêu. - Đầu ra: Trả về
Truenếu chuỗi khớp chính xác quy tắc số lượng ký tự lặp, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Chuỗi chỉ có 1 hoặc 2 chữ
z(như'wazup','wazzup') hoặc có từ 6 chữztrở lên (như'wazzzzzzup') đều bị loại bỏ hoàn toàn.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"waz{3,5}up"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["wazzzup", "wazzzzup", "wazzzzzup", "wazup", "wazzup", "wazzzzzzup"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"waz{3,5}up":- Ký tự tiền tố
wa: Bắt buộc xuất hiện chính xác hai ký tự này ở đầu chuỗi. - Định lượng khoảng
{3,5}: Tác động trực tiếp lên ký tựzđứng ngay trước nó, quy định ký tựzphải xuất hiện tối thiểu 3 lần và tối đa 5 lần. - Ký tự hậu tố
up: Bắt buộc xuất hiện hai ký tự này ở cuối chuỗi.
- Ký tự tiền tố
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Bảo đảm chuỗi không chứa thêm bất kỳ ký tự nào khác ở cả hai đầu. Nếu chuỗi có 2 ký tựzthì vi phạm cận dưới, còn nếu có 6 ký tựzthì vi phạm cận trên, tất cả đều được phát hiện chính xác.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'wazzzup' | True | bool |
'wazzzzup' | True | bool |
'wazzzzzup' | True | bool |
'wazup' | False | bool |
'wazzup' | False | bool |
'wazzzzzzup' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"waz{3,5}up")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["wazzzup", "wazzzzup", "wazzzzzup", "wazup", "wazzup", "wazzzzzzup"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét trọn vẹn chuỗi và trả về đối tượngMatchnếu số lần lặp thỏa mãn phạm vi số học.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'wazzzup' | True | bool |
'wazzzzup' | True | bool |
'wazzzzzup' | True | bool |
'wazup' | False | bool |
'wazzup' | False | bool |
'wazzzzzzup' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rất phù hợp cho các bài kiểm tra logic định dạng đơn giản trong tập lệnh phân tích dữ liệu. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 6: Phân biệt xuất hiện không lần và xuất hiện ít nhất một lần
Trong việc xác thực chuỗi ký hiệu trạng thái quy trình, chuỗi hợp lệ phải tuân theo cấu trúc tuần tự nghiêm ngặt: Có ít nhất hai ký tự a, theo sau là ký tự b xuất hiện tùy ý không lần hoặc nhiều lần, và kết thúc bằng ít nhất một ký tự c.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Kết hợp lượng từ mở rộng
{2,}, dấu sao*và dấu cộng+trong một mẫu biểu thức duy nhất để kiểm tra toàn bộ chuỗi. - Đầu ra: Trả về
Truenếu chuỗi thỏa mãn đúng cấu trúc và số lượng ký tự, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Chuỗi chỉ có một chữ
a(như'abc'), chuỗi thiếu hoàn toàn chữc(như'aabb'), hoặc chuỗi sai trật tự vị trí đều không hợp lệ.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"a{2,}b*c+"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["aaaabcc", "aabbbbc", "aacc", "a", "abc", "aabb"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"a{2,}b*c+":- Thành phần
a{2,}: Đặt cận dưới là hai ký tựamà không giới hạn cận trên, đòi hỏi tối thiểu hai chữaliên tiếp ở đầu chuỗi. - Thành phần
b*: Lượng từ sao cho phép ký tựbxuất hiện tùy ý từ không lần đến nhiều lần, do đó sự vắng mặt củab(như trong'aacc') vẫn hoàn toàn hợp lệ. - Thành phần
c+: Lượng từ cộng bắt buộc phải có ít nhất một ký tựcở cuối chuỗi.
- Thành phần
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Đảm bảo toàn bộ chuỗi phải khớp trọn vẹn cả ba khối ký tự theo đúng thứ tự xuất hiện từ trái qua phải mà không có ký tự lạ chen ngang.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'aaaabcc' | True | bool |
'aabbbbc' | True | bool |
'aacc' | True | bool |
'a' | False | bool |
'abc' | False | bool |
'aabb' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"a{2,}b*c+")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["aaaabcc", "aabbbbc", "aacc", "a", "abc", "aabb"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét tuần tự toàn chuỗi và kiểm tra điều kiện chuyển tiếp trạng thái.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'aaaabcc' | True | bool |
'aabbbbc' | True | bool |
'aacc' | True | bool |
'a' | False | bool |
'abc' | False | bool |
'aabb' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Trực quan, phù hợp khi kiểm tra quy tắc cú pháp tại một vị trí cục bộ trong hàm xử lý. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 7: Ký tự tùy chọn và dấu hỏi chấm thường trong thông báo
Trong việc kiểm tra tính chuẩn mực của các câu thông báo trạng thái tệp tin sinh ra từ hệ thống, câu thông báo hợp lệ bắt đầu bằng số lượng tệp (chữ số đầu từ 1 đến 9), theo sau bởi từ file hoặc files số nhiều, và kết thúc bằng chuỗi found? có dấu hỏi chấm thường.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng dấu hỏi
?để biểu thị ký tự tùy chọn cho chữsvà thoát ký tự dấu hỏi nguyên văn\?ở cuối câu thông báo. - Đầu ra: Trả về
Truenếu câu thông báo khớp trọn vẹn mẫu quy định, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Chữ số đầu tiên phải thuộc dải
[1-9], tức là từ chối số 0 hoặc số có số 0 đứng đầu như'0 files found?'. Dấu chấm câu ở cuối phải là dấu hỏi chấm literal, từ chối dấu chấm thường như'2 files found.'.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[1-9][0-9]* files? found\?"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"1 file found?",
"2 files found?",
"24 files found?",
"No files found.",
"0 files found?",
"2 files found.",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[1-9][0-9]* files? found\?":- Khối chữ số
[1-9][0-9]*: Ký tự đầu tiên thuộc tập[1-9]ngăn chặn hoàn toàn số 0 hoặc các số có số 0 ở đầu. Các ký tự số tiếp theo[0-9]*cho phép số lượng tệp có giá trị từ 1 đến hàng triệu. - Từ số nhiều tùy chọn
files?: Dấu hỏi?đứng ngay sau chữsbiến riêng chữ cáisthành tùy chọn, chấp nhận cả'file'(khi số lượng là 1) và'files'(khi số lượng từ 2 trở lên). - Thoát dấu hỏi kết thúc
found\?: Ký hiệu\?buộc ký tự cuối cùng của thông báo phải là dấu hỏi chấm thật trong bảng mã ASCII.
- Khối chữ số
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Kiểm tra từ đầu đến cuối câu thông báo, loại bỏ các chuỗi thừa từ hoặc sai dấu chấm câu ở cuối.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1 file found?' | True | bool |
'2 files found?' | True | bool |
'24 files found?' | True | bool |
'No files found.' | False | bool |
'0 files found?' | False | bool |
'2 files found.' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[1-9][0-9]* files? found\?")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"1 file found?",
"2 files found?",
"24 files found?",
"No files found.",
"0 files found?",
"2 files found.",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Kiểm tra toàn bộ chuỗi thông báo và trả về đối tượng khớp hoặcNone.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1 file found?' | True | bool |
'2 files found?' | True | bool |
'24 files found?' | True | bool |
'No files found.' | False | bool |
'0 files found?' | False | bool |
'2 files found.' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Thuận tiện khi viết nhanh các kịch bản kiểm tra đơn giản trong giai đoạn phát triển thử nghiệm. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 8: Ràng buộc ký tự phân cách khoảng trắng sau số thứ tự
Trong việc phân tích các dòng văn bản dạng danh mục có đánh số thứ tự, mỗi dòng hợp lệ phải gồm một số nguyên, theo sau là dấu chấm nguyên văn, bắt buộc có ít nhất một ký tự khoảng trắng phân cách (dấu cách hoặc ký tự tab), rồi đến chuỗi ký tự abc.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng lớp ký tự trắng
\s+để ràng buộc sự xuất hiện bắt buộc của khoảng trắng phân cách sau dấu chấm. - Đầu ra: Trả về
Truenếu chuỗi có số, dấu chấm, khoảng trắng phân cách vàabc. Ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Chuỗi dính liền không có khoảng trắng như
'4.abc'bị loại hoàn toàn. Ký tự phân cách phải là dấu chấm literal\., từ chối ký tự khác như'1x abc'.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[0-9]+\.\s+abc"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["1. abc", "2.\tabc", "3. abc", "4.abc", "12. abc", "1x abc"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[0-9]+\.\s+abc":- Khối số thứ tự
[0-9]+: Khớp một hoặc nhiều chữ số ASCII liên tiếp. - Thoát dấu chấm
\.: Đảm bảo dấu chấm là một ký tự literal, không phải ký tự đại diện wildcard. - Lớp ký tự trắng bắt buộc
\s+: Nhận diện một hoặc nhiều ký tự trắng liên tiếp (bao gồm dấu cách và tab\t). Việc dùng lượng từ+thay vì*là điều kiện tiên quyết để buộc phải có khoảng trống phân cách giữa số thứ tự và nội dung. - Nội dung cố định
abc: Bắt buộc kết thúc bằng đúng ba ký tự chữ cái này.
- Khối số thứ tự
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Đảm bảo toàn bộ dòng văn bản tuân thủ quy chuẩn, từ chối các dòng dính liền ký tự hoặc sai dấu chấm.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1. abc' | True | bool |
'2.\tabc' | True | bool |
'3. abc' | True | bool |
'4.abc' | False | bool |
'12. abc' | True | bool |
'1x abc' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[0-9]+\.\s+abc")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["1. abc", "2.\tabc", "3. abc", "4.abc", "12. abc", "1x abc"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét và kiểm tra tính toàn vẹn của chuỗi ký tự theo quy tắc phân định ranh giới.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1. abc' | True | bool |
'2.\tabc' | True | bool |
'3. abc' | True | bool |
'4.abc' | False | bool |
'12. abc' | True | bool |
'1x abc' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Tiện lợi cho các tác vụ xử lý dòng dữ liệu ngắn trong các sổ tay tính toán. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 9: Trích xuất tên tệp PDF bằng nhóm bắt độc lập
Trong việc bóc tách danh mục tài liệu lưu trữ, hệ thống cần trích xuất phần tên chính của các tệp tài liệu định dạng PDF. Tên tệp hợp lệ bắt buộc bắt đầu bằng tiền tố file, theo sau bởi ít nhất một ký tự chữ cái, chữ số hoặc gạch dưới, và kết thúc bằng phần mở rộng .pdf.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng nhóm bắt (capturing group) bao quanh phần tên tệp để lấy ra tên gốc mà không chứa phần mở rộng
.pdf. - Đầu ra: Trả về chuỗi
strchứa tên tệp (bao gồm cả tiền tốfile). Nếu chuỗi không phải tệp PDF hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Các tệp tạm thời có phần mở rộng kép như
'fileA.pdf.tmp'hoặc đường dẫn thư mục đều bị từ chối. Tên tệp không được chỉ có đúng từ'file'mà bắt buộc phải có thêm ký tự phía sau.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"(file[A-Za-z0-9_]+)\.pdf"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"file_record_transcript.pdf",
"file_07241999.pdf",
"fileA1.pdf",
"testfile_fake.pdf.tmp",
"fileA.pdf.tmp",
"fileA.txt",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"(file[A-Za-z0-9_]+)\.pdf":- Nhóm bắt
(file[A-Za-z0-9_]+): Cặp ngoặc đơn thiết lập nhóm bắt số 1, bao trọn tiền tốfilevà dãy ký tự tiếp theo gồm chữ cái, chữ số hoặc dấu gạch dưới. Lượng từ+yêu cầu có tối thiểu một ký tự sau tiền tốfile. - Phần mở rộng
\.pdf: Nằm bên ngoài nhóm bắt, yêu cầu chuỗi phải kết thúc bằng dấu chấm và chuỗipdf.
- Nhóm bắt
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Buộc toàn bộ chuỗi phải là tên tệp hoàn chỉnh, loại bỏ hoàn toàn các trường hợp tệp có thêm đuôi phụ như.pdf.tmp. - Phương thức
.group(1): Chỉ trích xuất chuỗi con nằm bên trong cặp ngoặc đơn đầu tiên, loại bỏ phần đuôi.pdfmà không cần xử lý cắt chuỗi thủ công.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'file_record_transcript.pdf' | 'file_record_transcript' | str |
'file_07241999.pdf' | 'file_07241999' | str |
'fileA1.pdf' | 'fileA1' | str |
'testfile_fake.pdf.tmp' | None | NoneType |
'fileA.pdf.tmp' | None | NoneType |
'fileA.txt' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"(file[A-Za-z0-9_]+)\.pdf")
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"file_record_transcript.pdf",
"file_07241999.pdf",
"fileA1.pdf",
"testfile_fake.pdf.tmp",
"fileA.pdf.tmp",
"fileA.txt",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Thực hiện so khớp toàn chuỗi và trả về đối tượngMatchchứa nhóm bắt tương ứng.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'file_record_transcript.pdf' | 'file_record_transcript' | str |
'file_07241999.pdf' | 'file_07241999' | str |
'fileA1.pdf' | 'fileA1' | str |
'testfile_fake.pdf.tmp' | None | NoneType |
'fileA.pdf.tmp' | None | NoneType |
'fileA.txt' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rõ ràng và tiện dụng khi xử lý danh sách tệp ngắn trong kịch bản lệnh dòng. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 10: Trích xuất số lượng phòng ngủ với ranh giới từ và cờ không phân biệt hoa thường
Trong việc khai thác thông tin từ các tiêu đề tin đăng chỗ ở trực tuyến, số lượng phòng ngủ có thể được diễn đạt dưới nhiều dạng viết tắt hoặc danh từ đầy đủ như 1BR, 3 bedrooms, 2 br. Cần trích xuất chính xác con số biểu thị số phòng mà không bị dính vào các mã chữ số khác.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng ranh giới từ
\b, nhóm không bắt(?:...)chứa phép hoặc và bật cờre.IGNORECASE(hoặcre.I) để tìm kiếm linh hoạt. - Đầu ra: Trả về chuỗi
strchứa con số chỉ số lượng phòng ngủ đầu tiên tìm thấy. Nếu không tìm thấy hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Ranh giới từ ở hai đầu loại bỏ các mã dính liền như
'12bravo'hoặc'2BR2BA'. Không trích xuất các con số biểu thị năm như'Torre 2024'vì không đi kèm đơn vị phòng.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"\b([0-9]+)\s*(?:BR|bedrooms?)\b"
def solve(text):
if text is None:
return None
m = re.search(PATTERN, text, flags=re.I)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"Modern 1BR Oasis",
"Casa 3 bedrooms",
"Studio 2 br centro",
"12bravo centro",
"Modern 2BR2BA",
"Torre 2024",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"\b([0-9]+)\s*(?:BR|bedrooms?)\b":- Ranh giới từ
\b: Đặt ở đầu và cuối mẫu nhằm bảo đảm cụm phòng ngủ là một từ độc lập, không bị lẫn vào các chuỗi số chữ liền kề như12bravohay mã kết hợp2BR2BA. - Nhóm bắt số lượng
([0-9]+): Bắt riêng dãy chữ số biểu thị số lượng phòng ngủ để lấy ra bằng.group(1). - Khoảng trắng tùy chọn
\s*: Cho phép viết liền (như1BR) hoặc cách rời (như3 bedrooms). - Nhóm không bắt đơn vị
(?:BR|bedrooms?): Cung cấp hai nhánh lựa chọn viết tắtBRhoặc viết đầy đủbedroom/bedroomsmà không tạo thêm nhóm bắt phụ trong kết quả. - Cờ
re.I(re.IGNORECASE): Cho phép khớp cả chữ hoa và chữ thường nhưbr,Br,BR,Bedrooms.
- Ranh giới từ
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.search(PATTERN, text, flags=re.I): Tìm kiếm cụm từ phòng ngủ tại bất kỳ vị trí nào trong chuỗi tiêu đề và trích xuất đúng con số quam.group(1).
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'Modern 1BR Oasis' | '1' | str |
'Casa 3 bedrooms' | '3' | str |
'Studio 2 br centro' | '2' | str |
'12bravo centro' | None | NoneType |
'Modern 2BR2BA' | None | NoneType |
'Torre 2024' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"\b([0-9]+)\s*(?:BR|bedrooms?)\b", flags=re.IGNORECASE)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.search(text)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"Modern 1BR Oasis",
"Casa 3 bedrooms",
"Studio 2 br centro",
"12bravo centro",
"Modern 2BR2BA",
"Torre 2024",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.search(text): Quét chuỗi nguồn và trích xuất nhóm bắt số 1 trực tiếp.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'Modern 1BR Oasis' | '1' | str |
'Casa 3 bedrooms' | '3' | str |
'Studio 2 br centro' | '2' | str |
'12bravo centro' | None | NoneType |
'Modern 2BR2BA' | None | NoneType |
'Torre 2024' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.search: Thuận tiện cho các phân tích khám phá nhanh trên tập mẫu dữ liệu nhỏ. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 11: Xác thực số nguyên và số thập phân đơn giản
Trong việc xác thực các trường nhập liệu số học từ bảng dữ liệu tài chính hoặc số lượng tồn kho, giá trị số hợp lệ có thể là số nguyên hoặc số thập phân, có thể mang dấu âm ở đầu. Chuỗi không được chứa chữ cái, đơn vị đo lường hoặc định dạng dở dang.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy kiểm tra toàn chuỗi với phần dấu âm tùy chọn và nhóm thập phân tùy chọn có điều kiện ràng buộc dấu chấm đi kèm chữ số.
- Đầu ra: Trả về
Truenếu chuỗi là số nguyên hoặc số thập phân hợp lệ, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Phần nguyên luôn bắt buộc phải có chữ số (từ chối chuỗi khuyết phần nguyên như
'.5'). Nếu có dấu chấm thập phân thì bắt buộc phải có chữ số đi kèm phía sau (từ chối'5.'). Các hậu tố như'720p'đều bị loại bỏ.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"-?[0-9]+(?:\.[0-9]+)?"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["3.14529", "-255.34", "128", "720p", ".5", "5."]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"-?[0-9]+(?:\.[0-9]+)?":- Dấu âm tùy chọn
-?: Cho phép số mang dấu âm hoặc không có dấu, nhưng không nhận dấu cộng ở mẫu này. - Phần nguyên bắt buộc
[0-9]+: Đòi hỏi ít nhất một chữ số ở phần nguyên, ngăn chặn lỗi thiếu phần nguyên như.5. - Nhóm thập phân không bắt
(?:\.[0-9]+)?: Dấu hỏi ở cuối biến toàn bộ cụm gồm dấu chấm và phần thập phân thành tùy chọn. Nếu dấu chấm xuất hiện thì lượng từ+buộc phải có ít nhất một chữ số đi liền sau nó, loại bỏ hoàn toàn các trường hợp viết dở như12..
- Dấu âm tùy chọn
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Kiểm tra từ ký tự đầu tiên đến ký tự cuối cùng của chuỗi, loại trừ các chuỗi chứa ký tự chữ cái như720phoặc ký tự xuống dòng dư thừa.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'3.14529' | True | bool |
'-255.34' | True | bool |
'128' | True | bool |
'720p' | False | bool |
'.5' | False | bool |
'5.' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"-?[0-9]+(?:\.[0-9]+)?")
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["3.14529", "-255.34", "128", "720p", ".5", "5."]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét toàn bộ chuỗi số và xác định ngay tính hợp lệ mà không cần kiểm tra lại cú pháp mẫu.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'3.14529' | True | bool |
'-255.34' | True | bool |
'128' | True | bool |
'720p' | False | bool |
'.5' | False | bool |
'5.' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Thuận tiện cho các hàm lọc đơn giản trong quá trình phân tích dữ liệu tương tác. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 12: Xác thực số có phân cách hàng nghìn và ký hiệu khoa học số mũ
Trong việc xử lý dữ liệu tài chính kế toán và dữ liệu đo đạc khoa học kỹ thuật, các con số thường được trình bày kèm dấu phân cách hàng nghìn bằng dấu phẩy (như 123,340.00) hoặc viết dưới dạng ký hiệu khoa học với số mũ (như 1.9e10, +1E-3).
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Xây dựng biểu thức chính quy toàn diện kết hợp dấu tùy chọn, cấu trúc phân cách hàng nghìn chuẩn 3 chữ số, phần thập phân tùy chọn và phần số mũ khoa học tùy chọn.
- Đầu ra: Trả về
Truenếu chuỗi biểu diễn đúng cấu trúc số hợp lệ, ngược lại trả vềFalse. Đầu vàoNonetrả vềFalse. - Trường hợp biên: Nếu dùng dấu phẩy phân cách hàng nghìn, mỗi nhóm sau dấu phẩy bắt buộc phải có đúng ba chữ số (chẳng hạn
'12,34.00'là sai quy chuẩn). Dấu cộng hoặc trừ có thể xuất hiện ở đầu số hoặc sau ký tự e/E trong số mũ.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[+-]?(?:[0-9]{1,3}(?:,[0-9]{3})+|[0-9]+)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"
def solve(text):
if text is None:
return False
m = re.fullmatch(PATTERN, text, flags=0)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["1.9e10", "123,340.00", "+1E-3", "12,34.00", "1,000e+2", "720p"]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[+-]?(?:[0-9]{1,3}(?:,[0-9]{3})+|[0-9]+)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?":- Dấu tùy chọn
[+-]?: Chấp nhận cả dấu cộng hoặc dấu trừ ở đầu số. - Nhánh phân tách phần nguyên
(?:[0-9]{1,3}(?:,[0-9]{3})+|[0-9]+): Nhánh thứ nhất nhận dạng có phân cách hàng nghìn, bắt đầu với 1 đến 3 chữ số, theo sau bởi một hoặc nhiều nhóm gồm dấu phẩy và đúng ba chữ số,[0-9]{3}. Nhánh thứ hai nhận dạng viết liền thông thường gồm các chữ số liên tiếp[0-9]+. - Phần thập phân tùy chọn
(?:\.[0-9]+)?: Bắt buộc có chữ số sau dấu chấm nếu xuất hiện dấu chấm. - Phần số mũ khoa học
(?:[eE][+-]?[0-9]+)?: Cho phép viếtehoặcE, theo sau là dấu tùy chọn và các chữ số biểu thị số mũ.
- Dấu tùy chọn
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Đảm bảo toàn bộ chuỗi được kiểm soát chặt chẽ, loại bỏ hoàn toàn các lỗi sai vị trí dấu phẩy hoặc định dạng số mũ thiếu chữ số.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1.9e10' | True | bool |
'123,340.00' | True | bool |
'+1E-3' | True | bool |
'12,34.00' | False | bool |
'1,000e+2' | True | bool |
'720p' | False | bool |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"[+-]?(?:[0-9]{1,3}(?:,[0-9]{3})+|[0-9]+)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"
)
def solve_compiled(text):
if text is None:
return False
m = COMPILED_PATTERN.fullmatch(text)
return m is not None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = ["1.9e10", "123,340.00", "+1E-3", "12,34.00", "1,000e+2", "720p"]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Thực thi kiểm tra cấu trúc chuỗi đầu vào theo tiêu chuẩn số học quốc tế.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'1.9e10' | True | bool |
'123,340.00' | True | bool |
'+1E-3' | True | bool |
'12,34.00' | False | bool |
'1,000e+2' | True | bool |
'720p' | False | bool |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Thích hợp cho các trường hợp kiểm thử nhanh một vài giá trị ngoại lai trong sổ tay dữ liệu. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 13: Trích xuất mã vùng điện thoại Bắc Mỹ với hai định dạng ngoặc đơn
Trong việc chuẩn hóa thông tin liên lạc khách hàng từ hệ thống tổng đài Bắc Mỹ, số điện thoại có thể bắt đầu bằng mã quốc gia 1 tùy chọn, theo sau là mã vùng 3 chữ số đặt trong cặp ngoặc đơn (416) hoặc viết liền 416, tiếp đến là 7 chữ số liên lạc có dấu cách hoặc gạch nối.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng hai nhánh bắt riêng biệt cho mã vùng: Một nhánh cho dạng có đủ cặp ngoặc đơn và một nhánh cho dạng không có ngoặc, bảo đảm không chấp nhận ngoặc đơn lẻ.
- Đầu ra: Trả về chuỗi
strgồm đúng 3 chữ số của mã vùng. Nếu số điện thoại sai định dạng hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Tuyệt đối không biến từng dấu ngoặc đơn thành tùy chọn độc lập (như
\(?...\)?), vì cách làm đó sẽ nhận nhầm các chuỗi chỉ có một ngoặc đơn lẻ như'(416555-3456'. Chuỗi sai quy cách số điện thoại đều phải trả vềNone.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"(?:1[ -]?)?(?:\(([0-9]{3})\)|([0-9]{3}))[ -]?[0-9]{3}[ -]?[0-9]{4}"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.group(1) or m.group(2) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"415-555-1234",
"(416)555-3456",
"202 555 4567",
"4035555678",
"1 416 555 9292",
"(416555-3456",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"(?:1[ -]?)?(?:\(([0-9]{3})\)|([0-9]{3}))[ -]?[0-9]{3}[ -]?[0-9]{4}":- Mã quốc gia tùy chọn
(?:1[ -]?)?: Nhận diện số 1 ở đầu kèm dấu cách hoặc dấu gạch nối nếu có. - Hai nhánh mã vùng
(?:\(([0-9]{3})\)|([0-9]{3})): Nhánh thứ nhất\(([0-9]{3})\)nhận dạng có đủ hai dấu ngoặc bao quanh và bắt 3 chữ số vào nhóm 1. Nhánh thứ hai([0-9]{3})nhận dạng không có ngoặc và bắt 3 chữ số vào nhóm 2. Việc tách thành hai nhánh nguyên vẹn giúp triệt tiêu hoàn toàn lỗi nhận ngoặc đơn lẻ. - Bảy chữ số còn lại
[ -]?[0-9]{3}[ -]?[0-9]{4}: Nhận diện 3 chữ số đầu và 4 chữ số cuối kèm dấu phân cách tùy ý.
- Mã quốc gia tùy chọn
- Phân tích hàm và giao thức gọi lệnh regex:
- Biểu thức
m.group(1) or m.group(2): Do hai nhánh loại trừ lẫn nhau, kết quả khớp sẽ nằm ở một trong hai nhóm bắt, cho phép lấy ra giá trị chuỗi không rỗng tương ứng.
- Biểu thức
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'415-555-1234' | '415' | str |
'(416)555-3456' | '416' | str |
'202 555 4567' | '202' | str |
'4035555678' | '403' | str |
'1 416 555 9292' | '416' | str |
'(416555-3456' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"(?:1[ -]?)?(?:\(([0-9]{3})\)|([0-9]{3}))[ -]?[0-9]{3}[ -]?[0-9]{4}"
)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.group(1) or m.group(2) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"415-555-1234",
"(416)555-3456",
"202 555 4567",
"4035555678",
"1 416 555 9292",
"(416555-3456",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Đối chiếu chuỗi điện thoại nguồn và cung cấp các nhóm bắt đã trích xuất.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'415-555-1234' | '415' | str |
'(416)555-3456' | '416' | str |
'202 555 4567' | '202' | str |
'4035555678' | '403' | str |
'1 416 555 9292' | '416' | str |
'(416555-3456' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rõ ràng, dễ hiểu khi kiểm tra một vài trường dữ liệu liên hệ riêng biệt. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 14: Trích xuất tên người dùng tài khoản email trước bộ lọc cộng
Trong việc phân tích và chuẩn hóa danh bạ thư điện tử người dùng, nhiều dịch vụ cho phép thêm phần gắn thẻ bộ lọc bằng dấu cộng sau tên người dùng (ví dụ: tom.riddle+regexone@hogwarts.com). Mục tiêu là trích xuất phần tên người dùng gốc (tom.riddle) phục vụ việc gom cụm định danh.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng nhóm bắt để thu thập phần tên tài khoản trước dấu cộng tùy chọn và kiểm tra tính hợp lệ của miền email giản lược.
- Đầu ra: Trả về chuỗi
strchứa tên tài khoản cơ sở. Nếu địa chỉ email không hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Tên tài khoản không được có hai dấu chấm liên tiếp (như
'tom..riddle@hogwarts.com'). Phần gắn thẻ+bo_lockhông thuộc nhóm bắt. Tên miền phải có tên miền cấp cao nhất (TLD) tối thiểu hai chữ cái.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)(?:\+[A-Za-z0-9_]+)?@(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.)+[A-Za-z]{2,}"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"tom@hogwarts.com",
"tom.riddle@hogwarts.com",
"tom.riddle+regexone@hogwarts.com",
"tom@hogwarts.eu.com",
"hermione+regexone@hogwarts.com",
"tom..riddle@hogwarts.com",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)(?:\+[A-Za-z0-9_]+)?@(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.)+[A-Za-z]{2,}":- Nhóm bắt tên cơ sở
([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*): Bắt chuỗi định danh gồm các đoạn chữ, số hoặc gạch dưới. Giữa các đoạn là đúng một dấu chấm nhờ cấu trúc(?:\.[A-Za-z0-9_]+)*, giúp loại bỏ triệt để trường hợp hai dấu chấm kề nhau như... - Thẻ bộ lọc tùy chọn
(?:\+[A-Za-z0-9_]+)?: Đặt trong nhóm không bắt nằm ngoài nhóm 1, bảo đảm thẻ sau dấu cộng được kiểm tra tính hợp lệ nhưng không bị gom vào tên gốc. - Tên miền email
@(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.)+[A-Za-z]{2,}: Kiểm tra tên miền phân tách bằng dấu chấm, hỗ trợ tên miền con đa cấp và đuôi tên miền quốc tế từ hai chữ cái trở lên.
- Nhóm bắt tên cơ sở
- Phân tích hàm và giao thức gọi lệnh regex:
- Phương thức
m.group(1): Lấy chính xác tên tài khoản trước bộ lọc, bảo đảm tính toàn vẹn cho dữ liệu định danh khách hàng.
- Phương thức
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'tom@hogwarts.com' | 'tom' | str |
'tom.riddle@hogwarts.com' | 'tom.riddle' | str |
'tom.riddle+regexone@hogwarts.com' | 'tom.riddle' | str |
'tom@hogwarts.eu.com' | 'tom' | str |
'hermione+regexone@hogwarts.com' | 'hermione' | str |
'tom..riddle@hogwarts.com' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)(?:\+[A-Za-z0-9_]+)?@(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.)+[A-Za-z]{2,}"
)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.group(1) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"tom@hogwarts.com",
"tom.riddle@hogwarts.com",
"tom.riddle+regexone@hogwarts.com",
"tom@hogwarts.eu.com",
"hermione+regexone@hogwarts.com",
"tom..riddle@hogwarts.com",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét và trích xuất nhóm bắt số 1 an toàn trên toàn bộ chuỗi ký tự.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'tom@hogwarts.com' | 'tom' | str |
'tom.riddle@hogwarts.com' | 'tom.riddle' | str |
'tom.riddle+regexone@hogwarts.com' | 'tom.riddle' | str |
'tom@hogwarts.eu.com' | 'tom' | str |
'hermione+regexone@hogwarts.com' | 'hermione' | str |
'tom..riddle@hogwarts.com' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rất phù hợp khi phân tích dữ liệu dạng ad-hoc trên các tập dữ liệu nhỏ. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 15: Trích xuất tên thẻ mở HTML đầu tiên và bỏ qua đoạn chú thích
Trong việc bóc tách nội dung thô từ các trang web hoặc tài liệu đánh dấu bán cấu trúc, hệ thống cần trích xuất tên của thẻ mở HTML đầu tiên (như a, div, span). Trước khi tìm thẻ, quy trình kỹ thuật bắt buộc phải loại bỏ các đoạn chú thích <!-- ... --> để không nhận nhầm thẻ giả nằm trong phần ghi chú.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Phân tách và bỏ qua các đoạn chú thích HTML đã đóng, sau đó dùng biểu thức chính quy trích xuất tên thẻ mở đầu tiên bắt đầu bằng chữ cái.
- Đầu ra: Trả về chuỗi
strchứa tên thẻ mở đầu tiên. Nếu không tìm thấy thẻ hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Thẻ đóng như
'</div>'bị bỏ qua vì mẫu yêu cầu tên thẻ phải đứng ngay sau dấu<. Thẻ tự đóng như'<br/>'hoặc thẻ có thuộc tính như"<a href='...'>"đều được xử lý chính xác.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"<([A-Za-z][A-Za-z0-9-]*)(?:\s+[^<>]*?)?\s*/?>"
def solve(text):
if text is None:
return None
for i, part in enumerate(text.split("<!--")):
if i:
_, closed, part = part.partition("-->")
if not closed:
return None
m = re.search(PATTERN, part, flags=0)
if m is not None:
return m.group(1)
return None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"<a>Liên kết</a>",
"<a href='https://regexone.com'>Mở</a>",
"<div class='test_style'>Nội dung</div>",
"<div>Chào <span>lớp</span></div>",
"</div>",
"văn bản thuần",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"<([A-Za-z][A-Za-z0-9-]*)(?:\s+[^<>]*?)?\s*/?>":- Dấu mở và tên thẻ
<([A-Za-z][A-Za-z0-9-]*): Dấu mở<đứng liền trước tên thẻ. Tên thẻ bắt buộc bắt đầu bằng một chữ cái[A-Za-z]và có thể chứa chữ số hoặc gạch nối[A-Za-z0-9-]. Việc không cho phép dấu gạch chéo/ngay sau<giúp mẫu tự động loại bỏ các thẻ đóng</...>một cách tự nhiên. - Thuộc tính tùy chọn
(?:\s+[^<>]*?)?: Chấp nhận các thuộc tính nhưhref,classnằm giữa khoảng trắng và dấu đóng thẻ. - Dấu đóng thẻ
\s*/?>: Chấp nhận cả thẻ mở thông thường>và thẻ tự đóng/>.
- Dấu mở và tên thẻ
- Phân tích hàm và giao thức gọi lệnh regex:
- Phép phân tách chú thích
.split('<!--')và.partition('-->'): Đảm bảo toàn bộ nội dung nằm trong chú thích HTML được loại bỏ hoàn toàn trước khi áp dụng biểu thức chính quy. Nếu có chú thích mở mà không đóng, hàm trả vềNonean toàn. - Hàm
re.search(PATTERN, part, flags=0): Tìm thẻ mở đầu tiên xuất hiện trong các phần văn bản thực tế ngoài chú thích.
- Phép phân tách chú thích
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'<a>Liên kết</a>' | 'a' | str |
"<a href='https://regexone.com'>Mở</a>" | 'a' | str |
"<div class='test_style'>Nội dung</div>" | 'div' | str |
'<div>Chào <span>lớp</span></div>' | 'div' | str |
'</div>' | None | NoneType |
'văn bản thuần' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"<([A-Za-z][A-Za-z0-9-]*)(?:\s+[^<>]*?)?\s*/?>")
def solve_compiled(text):
if text is None:
return None
for i, part in enumerate(text.split("<!--")):
if i:
_, closed, part = part.partition("-->")
if not closed:
return None
m = COMPILED_PATTERN.search(part)
if m is not None:
return m.group(1)
return None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"<a>Liên kết</a>",
"<a href='https://regexone.com'>Mở</a>",
"<div class='test_style'>Nội dung</div>",
"<div>Chào <span>lớp</span></div>",
"</div>",
"văn bản thuần",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.search(part): Thực hiện quét từng đoạn phân tách của văn bản một cách trực tiếp.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'<a>Liên kết</a>' | 'a' | str |
"<a href='https://regexone.com'>Mở</a>" | 'a' | str |
"<div class='test_style'>Nội dung</div>" | 'div' | str |
'<div>Chào <span>lớp</span></div>' | 'div' | str |
'</div>' | None | NoneType |
'văn bản thuần' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.search: Dễ dàng lồng ghép trong các hàm tiện ích nhỏ xử lý văn bản tạm thời. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 16: Lọc tệp hình ảnh hợp lệ và trích xuất cặp tên tệp cùng phần mở rộng
Trong việc quản lý kho tệp đa phương tiện của hệ thống chỗ ở, các tệp ảnh hợp lệ bắt buộc có phần mở rộng thuộc một trong ba định dạng hình ảnh chuẩn: jpg, png hoặc gif. Mục tiêu là tách riêng phần tên tệp cơ sở và phần mở rộng thành một cặp giá trị tuple phục vụ việc lập tệp.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng hai nhóm bắt độc lập: Nhóm thứ nhất bắt phần tên tệp (có thể chứa dấu chấm hoặc gạch nối ở giữa các phân đoạn) và nhóm thứ hai bắt đúng phần mở rộng ảnh cho phép.
- Đầu ra: Trả về bộ giá trị tuple dạng
(ten_tep, phan_mo_rong)nếu chuỗi là tệp ảnh hợp lệ. Nếu không hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Tên tệp không được bắt đầu hoặc kết thúc bằng dấu chấm (loại bỏ tệp ẩn như
'.bash_profile'). Các tệp văn bản khác (như'workspace.doc') hoặc tệp có đuôi tạm (như'img0912.jpg.tmp') đều bị từ chối.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"([A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)*)\.(jpg|png|gif)"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.groups() if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"img0912.jpg",
"updated_img0912.png",
"favicon.gif",
".bash_profile",
"workspace.doc",
"img0912.jpg.tmp",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"([A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)*)\.(jpg|png|gif)":- Nhóm bắt tên tệp
([A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)*): Nhóm bắt số 1 nhận các đoạn ký tự chữ, số hoặc gạch dưới. Giữa các đoạn có thể chứa dấu chấm hoặc gạch nối nhờ cấu trúc(?:[.-][A-Za-z0-9_]+)*. Điều này cho phép các tên tệp phức hợp nhưanh-phong.01nhưng không cho phép dấu chấm ở đầu hoặc cuối tên tệp. - Thoát dấu chấm
\.: Ranh giới phân cách giữa tên tệp và phần mở rộng. - Nhóm bắt phần mở rộng
(jpg|png|gif): Nhóm bắt số 2 quy định chỉ chấp nhận đúng ba định dạng ảnh được phép viết thường.
- Nhóm bắt tên tệp
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.fullmatch(PATTERN, text, flags=0): Đảm bảo phần mở rộng nằm ở cuối chuỗi, loại trừ tệp có thêm đuôi phụ như.tmp. - Phương thức
m.groups(): Trả về một bộ tuple chứa đầy đủ các nhóm bắt(nhóm_1, nhóm_2), khớp chính xác với kiểu dữ liệu mong đợi của đề bài.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'img0912.jpg' | ('img0912', 'jpg') | tuple |
'updated_img0912.png' | ('updated_img0912', 'png') | tuple |
'favicon.gif' | ('favicon', 'gif') | tuple |
'.bash_profile' | None | NoneType |
'workspace.doc' | None | NoneType |
'img0912.jpg.tmp' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"([A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)*)\.(jpg|png|gif)")
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.groups() if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"img0912.jpg",
"updated_img0912.png",
"favicon.gif",
".bash_profile",
"workspace.doc",
"img0912.jpg.tmp",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét trọn vẹn chuỗi và cung cấp ngay tuple(m.group(1), m.group(2))qua phương thức.groups().
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'img0912.jpg' | ('img0912', 'jpg') | tuple |
'updated_img0912.png' | ('updated_img0912', 'png') | tuple |
'favicon.gif' | ('favicon', 'gif') | tuple |
'.bash_profile' | None | NoneType |
'workspace.doc' | None | NoneType |
'img0912.jpg.tmp' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Trực quan và dễ dùng cho các hàm lọc tệp trong tập lệnh bảo trì hệ thống. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 17: Khắc phục tính tham lam trong biểu thức cắt tỉa khoảng trắng hai đầu
Trong việc chuẩn hóa các chuỗi văn bản tự do, yêu cầu kỹ thuật là loại bỏ các ký tự dấu cách và ký tự tab ở hai đầu chuỗi, đồng thời bảo toàn nguyên vẹn các khoảng trắng nằm giữa các từ cũng như các ký tự trắng đặc biệt khác. Cần dùng biểu thức chính quy phi tham lam để nhóm trung tâm không nuốt mất khoảng trắng cần cắt ở đuôi.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng lớp ký tự
[ \t]*ở hai đầu và nhóm bắt không tham lam(.*?)ở giữa để lấy nội dung bên trong. - Đầu ra: Trả về chuỗi
strđã được cắt tỉa khoảng trắng hai đầu. Nếu chuỗi ban đầu chỉ gồm toàn dấu cách và tab, trả về chuỗi rỗng''. Nếu đầu vào làNone, trả vềNone. - Trường hợp biên: Chỉ cắt dấu cách và ký tự tab
\t, không dùng\sđể tránh xóa các ký tự đặc biệt khác theo yêu cầu riêng của đề bài. Nhóm bắt phải dùng phép lặp lười (lazy) để nhường các khoảng trắng ở cuối cho lớp[ \t]*phía sau.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"[ \t]*(.*?)[ \t]*"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return (m.group(1) or "") if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [" Chào lớp ", "\tPhân tích dữ liệu\t", " A B ", " ", "", None]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"[ \t]*(.*?)[ \t]*":- Khối cắt đầu
[ \t]*: Nhận diện và tiêu thụ toàn bộ các ký tự dấu cách và tab xuất hiện liên tiếp ở đầu chuỗi. - Nhóm bắt phi tham lam
(.*?): Dấu hỏi sau dấu sao chuyển chế độ lặp từ tham lam (greedy) sang lười (lazy/non-greedy). Nhóm này sẽ chỉ nhận số lượng ký tự tối thiểu cần thiết để phần còn lại của chuỗi có thể khớp với khối cắt đuôi. Nếu dùng(.*)tham lam, nhóm giữa sẽ nuốt luôn các dấu cách ở cuối chuỗi. - Khối cắt đuôi
[ \t]*: Tiêu thụ toàn bộ dấu cách và tab còn lại ở cuối chuỗi.
- Khối cắt đầu
- Phân tích hàm và giao thức gọi lệnh regex:
- Biểu thức
(m.group(1) or ""): Đảm bảo khi chuỗi ban đầu rỗng hoặc chỉ toàn khoảng trắng, kết quả trả về là chuỗi rỗng""thay vì giá trịNone.
- Biểu thức
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
' Chào lớp ' | 'Chào lớp' | str |
'\tPhân tích dữ liệu\t' | 'Phân tích dữ liệu' | str |
' A B ' | 'A B' | str |
' ' | '' | str |
'' | '' | str |
None | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(r"[ \t]*(.*?)[ \t]*")
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return (m.group(1) or "") if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [" Chào lớp ", "\tPhân tích dữ liệu\t", " A B ", " ", "", None]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét chuỗi và cung cấp nhóm trích xuất nội dung đã cắt tỉa.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
' Chào lớp ' | 'Chào lớp' | str |
'\tPhân tích dữ liệu\t' | 'Phân tích dữ liệu' | str |
' A B ' | 'A B' | str |
' ' | '' | str |
'' | '' | str |
None | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Thuận tiện cho các thao tác làm sạch dữ liệu nhanh trong sổ tay thực hành. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 18: Phân tích dòng vết ngăn xếp lỗi để trích xuất hàm, tệp nguồn và số dòng
Trong việc giám sát vận hành và tự động hóa xử lý sự cố từ nhật ký hệ thống Android/Java, các dòng vết ngăn xếp lỗi (stack trace) chứa thông tin vị trí phát sinh lỗi dạng: at package.Class.method(FileName.java:123). Mục tiêu là trích xuất phương thức gọi, tên tệp Java và chuyển số dòng thành số nguyên int.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy với ranh giới từ
\b, các nhóm không bắt cho đường dẫn gói lớp và ba nhóm bắt độc lập cho phương thức, tệp nguồn và số dòng. - Đầu ra: Trả về bộ giá trị tuple dạng
(ten_phuong_thuc, ten_tep_java, so_dong)trong đóso_dongthuộc kiểu số nguyênint. Nếu dòng không phải vết ngăn xếp lỗi hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Các dòng thông báo không chứa vị trí dòng lỗi (như thông báo ngoại lệ chung) hoặc dòng có số dòng không phải chữ số ASCII đều bị từ chối.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"\bat\s+(?:[A-Za-z_][A-Za-z0-9_]*\.)+([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z0-9_]+\.java):([0-9]+)\)"
def solve(text):
if text is None:
return None
m = re.search(PATTERN, text, flags=0)
return (m.group(1), m.group(2), int(m.group(3))) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"E/( 1553): at widget.List.makeView(ListView.java:1727)",
"E/( 1553): at widget.List.fillDown(ListView.java:652)",
"E/( 1553): at widget.List.fillFrom(ListView.java:709)",
"W/dalvikvm: uncaught exception",
"E/: FATAL EXCEPTION: main",
"at widget.List.run(ListView.java:abc)",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"\bat\s+(?:[A-Za-z_][A-Za-z0-9_]*\.)+([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z0-9_]+\.java):([0-9]+)\)":- Ranh giới từ và từ khóa
\bat\s+: Xác định ranh giới bắt đầu dòng vết lỗi với từ khóaattheo sau bởi ít nhất một khoảng trắng. - Chuỗi gói và lớp
(?:[A-Za-z_][A-Za-z0-9_]*\.)+: Nhóm không bắt nhận một hoặc nhiều phân đoạn tên gói hợp lệ có dấu chấm đi kèm phía sau. - Nhóm bắt tên phương thức
([A-Za-z_][A-Za-z0-9_]*): Nhóm bắt số 1 lấy tên định danh phương thức Java. - Nhóm bắt tệp nguồn
\(([A-Za-z0-9_]+\.java): Thoát dấu ngoặc mở\(và bắt tên tệp kết thúc bằng.javavào nhóm số 2. - Nhóm bắt số dòng
:([0-9]+)\): Thoát dấu hai chấm, bắt chuỗi chữ số vào nhóm số 3 và thoát dấu ngoặc đóng\).
- Ranh giới từ và từ khóa
- Phân tích hàm và giao thức gọi lệnh regex:
- Hàm
re.search(PATTERN, text, flags=0): Tìm đoạn vết ngăn xếp tại bất kỳ vị trí nào trên dòng log. - Ép kiểu số nguyên
int(m.group(3)): Chuyển đổi chuỗi chữ số sang số nguyêninttheo đúng yêu cầu cấu trúc dữ liệu của bài toán.
- Hàm
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'E/( 1553): at widget.List.makeView(ListView.java:1727)' | ('makeView', 'ListView.java', 1727) | tuple |
'E/( 1553): at widget.List.fillDown(ListView.java:652)' | ('fillDown', 'ListView.java', 652) | tuple |
'E/( 1553): at widget.List.fillFrom(ListView.java:709)' | ('fillFrom', 'ListView.java', 709) | tuple |
'W/dalvikvm: uncaught exception' | None | NoneType |
'E/: FATAL EXCEPTION: main' | None | NoneType |
'at widget.List.run(ListView.java:abc)' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"\bat\s+(?:[A-Za-z_][A-Za-z0-9_]*\.)+([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z0-9_]+\.java):([0-9]+)\)"
)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.search(text)
return (m.group(1), m.group(2), int(m.group(3))) if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"E/( 1553): at widget.List.makeView(ListView.java:1727)",
"E/( 1553): at widget.List.fillDown(ListView.java:652)",
"E/( 1553): at widget.List.fillFrom(ListView.java:709)",
"W/dalvikvm: uncaught exception",
"E/: FATAL EXCEPTION: main",
"at widget.List.run(ListView.java:abc)",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.search(text): Quét dòng nhật ký trực tiếp và cung cấp ba nhóm bắt.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'E/( 1553): at widget.List.makeView(ListView.java:1727)' | ('makeView', 'ListView.java', 1727) | tuple |
'E/( 1553): at widget.List.fillDown(ListView.java:652)' | ('fillDown', 'ListView.java', 652) | tuple |
'E/( 1553): at widget.List.fillFrom(ListView.java:709)' | ('fillFrom', 'ListView.java', 709) | tuple |
'W/dalvikvm: uncaught exception' | None | NoneType |
'E/: FATAL EXCEPTION: main' | None | NoneType |
'at widget.List.run(ListView.java:abc)' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.search: Thuận tiện khi viết nhanh các kịch bản đọc tệp log nhỏ phục vụ phân tích sự cố tức thời. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 19: Phân tích cú pháp địa chỉ URL thành giao thức, máy chủ và cổng mạng
Trong việc phân tích lưu lượng mạng và nhật ký truy cập máy chủ web, mỗi địa chỉ URL hợp lệ cần được phân tách thành các thành phần cốt lõi: Giao thức mạng (scheme), tên máy chủ (host) và cổng mạng (port) nếu có chỉ định rõ ràng.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy kiểm tra toàn chuỗi với ba nhóm bắt cho giao thức, máy chủ và cổng mạng.
- Đầu ra: Trả về bộ giá trị tuple dạng
(giao_thuc, may_chu, cong_mang)trong đócong_manglà chuỗi chữ số hoặcNonenếu URL không ghi cổng. Nếu URL không hợp lệ hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Dấu hai chấm của cổng mạng nằm ngoài nhóm bắt số 3 nhưng nằm trong nhóm tùy chọn, bảo đảm khi không có cổng thì nhóm 3 trả về
None. Phần đường dẫn sau máy chủ không được chứa khoảng trắng.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"([A-Za-z][A-Za-z0-9+.-]*)://([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)(?::([0-9]+))?(?:/[^\s]*)?"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.groups() if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"ftp://file_server.com:21/top_secret/a.pdf",
"https://regexone.com/lesson/introduction#section",
"file://localhost:4040/zip_file",
"https://s3cur3-server.com:9999/",
"market://search/angry%20birds",
"https://host:abc/a",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"([A-Za-z][A-Za-z0-9+.-]*)://([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)(?::([0-9]+))?(?:/[^\s]*)?":- Nhóm bắt giao thức
([A-Za-z][A-Za-z0-9+.-]*): Nhóm bắt số 1 lấy tên giao thức mạng (nhưftp,https,file,git+ssh), bắt đầu bằng chữ cái và cho phép dấu cộng, dấu chấm hoặc gạch nối. - Chuỗi phân cách
://: Ranh giới cố định giữa giao thức và máy chủ. - Nhóm bắt máy chủ
([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*): Nhóm bắt số 2 lấy tên miền hoặc địa chỉ máy chủ, hỗ trợ tên miền con đa cấp phân tách bằng dấu chấm. - Nhóm bắt cổng mạng tùy chọn
(?::([0-9]+))?: Dấu hai chấm nằm ngoài nhóm bắt số 3([0-9]+). Khi URL có cổng (như:21), nhóm 3 nhận đúng chuỗi'21'. Khi URL không có cổng, nhóm 3 nhận giá trịNone. - Đường dẫn tài nguyên tùy chọn
(?:/[^\s]*)?: Cho phép có phần đường dẫn bắt đầu bằng dấu gạch chéo và không chứa khoảng trắng.
- Nhóm bắt giao thức
- Phân tích hàm và giao thức gọi lệnh regex:
- Phương thức
m.groups(): Trả về một tuple đúng 3 phần tử tương ứng với 3 nhóm bắt(giao_thức, máy_chủ, cổng).
- Phương thức
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'ftp://file_server.com:21/top_secret/a.pdf' | ('ftp', 'file_server.com', '21') | tuple |
'https://regexone.com/lesson/introduction#section' | ('https', 'regexone.com', None) | tuple |
'file://localhost:4040/zip_file' | ('file', 'localhost', '4040') | tuple |
'https://s3cur3-server.com:9999/' | ('https', 's3cur3-server.com', '9999') | tuple |
'market://search/angry%20birds' | ('market', 'search', None) | tuple |
'https://host:abc/a' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"([A-Za-z][A-Za-z0-9+.-]*)://([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)(?::([0-9]+))?(?:/[^\s]*)?"
)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.groups() if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"ftp://file_server.com:21/top_secret/a.pdf",
"https://regexone.com/lesson/introduction#section",
"file://localhost:4040/zip_file",
"https://s3cur3-server.com:9999/",
"market://search/angry%20birds",
"https://host:abc/a",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét trọn vẹn chuỗi URL và trả về các thành phần đã tách nhóm.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'ftp://file_server.com:21/top_secret/a.pdf' | ('ftp', 'file_server.com', '21') | tuple |
'https://regexone.com/lesson/introduction#section' | ('https', 'regexone.com', None) | tuple |
'file://localhost:4040/zip_file' | ('file', 'localhost', '4040') | tuple |
'https://s3cur3-server.com:9999/' | ('https', 's3cur3-server.com', '9999') | tuple |
'market://search/angry%20birds' | ('market', 'search', None) | tuple |
'https://host:abc/a' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Phù hợp cho việc kiểm tra URL trong các hàm phân tích nhật ký độc lập. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Bài 20: Trích xuất đường dẫn tài nguyên URL và loại bỏ tham số truy vấn cùng đoạn neo
Trong việc phân tích hành vi người dùng trên website thông qua nhật ký máy chủ web, các địa chỉ URL truy cập thường đi kèm tham số truy vấn phức tạp (sau dấu ?) hoặc đoạn neo định vị trang (sau dấu #). Mục tiêu là trích xuất đúng phần đường dẫn tài nguyên nội bộ (path) và loại bỏ hoàn toàn các phần phụ phía sau.
Yêu cầu kỹ thuật:
- Đầu vào: Chuỗi văn bản
textthuộc kiểustrhoặc giá trị khuyết thiếuNone. - Thao tác: Sử dụng biểu thức chính quy kiểm tra toàn chuỗi URL hợp lệ bắt đầu bằng
httphoặchttps, sử dụng nhóm bắt riêng biệt cho phần đường dẫn tài nguyên bắt đầu bằng dấu gạch chéo và dừng trước dấu hỏi hoặc dấu thăng. - Đầu ra: Trả về chuỗi
strchứa đường dẫn tài nguyên. Nếu URL hợp lệ nhưng không ghi đường dẫn cụ thể, mặc định trả về'/'. Nếu URL sai định dạng hoặc đầu vào làNone, trả vềNone. - Trường hợp biên: Các giao thức khác ngoài HTTP/HTTPS (như
'ftp://example.com/a') đều bị từ chối. Đường dẫn phải giữ nguyên các ký tự mã hóa URL (chẳng hạn'%20'). Chuỗi truy vấn và đoạn neo phải được phân tách bằng nhóm không bắt riêng biệt.
Lời giải
Cách 1: Người mới bắt đầu (Gọi hàm trực tiếp từ module re)
Người học gọi trực tiếp hàm xử lý từ module re của thư viện chuẩn Python để kiểm tra hoặc trích xuất chuỗi.
python
import re
PATTERN = r"https?://[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*(?::[0-9]+)?(/[^?#\s]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?"
def solve(text):
if text is None:
return None
m = re.fullmatch(PATTERN, text, flags=0)
return m.group(1) or "/" if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"https://example.com/data.csv?year=2026#top",
"http://localhost:8080/a/b",
"https://example.com",
"https://example.com/?q=x",
"https://example.com/angry%20birds#info",
"ftp://example.com/a",
]
for s in mau_kiem_thu:
print(f"solve({repr(s)}) = {repr(solve(s))}")Giải thích chi tiết Cách 1:
- Phân tích mẫu biểu thức chính quy
r"https?://[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*(?::[0-9]+)?(/[^?#\s]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?":- Giao thức
https?://: Chỉ chấp nhận hai giao thức web tiêu chuẩn làhttphoặchttps. - Máy chủ và cổng
[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*(?::[0-9]+)?: Kiểm tra tên máy chủ hợp lệ kèm cổng tùy chọn. - Nhóm bắt đường dẫn
(/[^?#\s]*)?: Bắt đầu bằng dấu gạch chéo/, theo sau bởi các ký tự không phải là dấu hỏi?, dấu thăng#hay khoảng trắng. Nhờ đó, đường dẫn dừng lại chính xác trước khi bước sang phần truy vấn hoặc đoạn neo. - Nhóm truy vấn tùy chọn
(?:\?[^#\s]*)?: Nhận diện chuỗi truy vấn bắt đầu bằng dấu hỏi mà không bị lẫn vào đường dẫn. - Nhóm đoạn neo tùy chọn
(?:#[^\s]*)?: Nhận diện đoạn neo định vị bắt đầu bằng dấu thăng.
- Giao thức
- Phân tích hàm và giao thức gọi lệnh regex:
- Biểu thức
m.group(1) or "/": Nếu URL kết thúc ngay sau máy chủ mà không có đường dẫn (nhưhttps://example.com),m.group(1)mang giá trịNonevà toán tửorsẽ tự động trả về giá trị mặc định chuẩn mực là'/'.
- Biểu thức
Kết quả thực thi Cách 1:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'https://example.com/data.csv?year=2026#top' | '/data.csv' | str |
'http://localhost:8080/a/b' | '/a/b' | str |
'https://example.com' | '/' | str |
'https://example.com/?q=x' | '/' | str |
'https://example.com/angry birds#info' | '/angry%20birds' | str |
'ftp://example.com/a' | None | NoneType |
Cách 2: Ứng dụng thực tế (Biên dịch mẫu trước với re.compile)
Mẫu được đặt trong một đối tượng Pattern để dùng lại trong hàm xử lý.
python
import re
COMPILED_PATTERN = re.compile(
r"https?://[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*(?::[0-9]+)?(/[^?#\s]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?"
)
def solve_compiled(text):
if text is None:
return None
m = COMPILED_PATTERN.fullmatch(text)
return m.group(1) or "/" if m is not None else None
# Dữ liệu kiểm thử độc lập
mau_kiem_thu = [
"https://example.com/data.csv?year=2026#top",
"http://localhost:8080/a/b",
"https://example.com",
"https://example.com/?q=x",
"https://example.com/angry%20birds#info",
"ftp://example.com/a",
]
for s in mau_kiem_thu:
print(f"solve_compiled({repr(s)}) = {repr(solve_compiled(s))}")Giải thích chi tiết Cách 2:
re.compiletrả về đối tượngPatternchứa mẫu và các cờ đã chọn. Gọi phương thức trên đối tượng này để giữ cùng quy tắc cho mọi chuỗi.- Phương thức
COMPILED_PATTERN.fullmatch(text): Quét toàn bộ chuỗi URL và bóc tách nhóm bắt số 1 một cách tin cậy.
Kết quả thực thi Cách 2:
| Đầu vào | Kết quả trả về | Kiểu kết quả |
|---|---|---|
'https://example.com/data.csv?year=2026#top' | '/data.csv' | str |
'http://localhost:8080/a/b' | '/a/b' | str |
'https://example.com' | '/' | str |
'https://example.com/?q=x' | '/' | str |
'https://example.com/angry birds#info' | '/angry%20birds' | str |
'ftp://example.com/a' | None | NoneType |
So sánh hai phương pháp:
- Gọi trực tiếp
re.fullmatch: Rất phù hợp cho các bài phân tích tương tác trên tập dữ liệu web quy mô nhỏ. - Biên dịch mẫu với
re.compile: Dùng khi cần đặt tên cho quy tắc và dùng chung mẫu trong nhiều lần gọi. Hãy đo trên dữ liệu thực tế nếu cần so sánh tốc độ.
Lab 10: Kiểm tra chất lượng và làm sạch dữ liệu chéo bảng
Hai bảng có thể đọc được nhưng chưa khớp về mã chỗ ở, ngày hoặc cách đếm đánh giá. Ta sẽ lập các phép kiểm, giữ dấu vết dòng bị gắn cờ và phân biệt phát hiện vấn đề với quyết định sửa dữ liệu.
Bối cảnh và snapshot dữ liệu tham chiếu
Các bài tập thực hành trong bài học này đối chiếu trực tiếp với bộ dữ liệu lưu trữ công cộng của Inside Airbnb đối với thành phố Santiago, Chile:
- Địa chỉ kho dữ liệu gốc:
https://data.insideairbnb.com/chile/rm/santiago/ - Phiên bản snapshot tham chiếu: Bản lưu trữ ngày 29 tháng 06 năm 2026 (
2026-06-29/visualisations/listings.csvvà2026-06-29/visualisations/reviews.csv). - Đơn vị đo lường: Giá phòng trong bảng chỗ ở được niêm yết bằng đồng Peso Chile (CLP/đêm) làm tròn đến số nguyên, và trong một số kịch bản giá đa kỳ thử nghiệm, giá được quy đổi theo đơn vị USD/đêm.
NOTE
Quy ước dữ liệu mẫu trong Lab 10: Nhằm phục vụ mục đích kiểm chứng từng bước thuật toán, tính toán nhẩm đối chiếu bằng tay và chạy kiểm thử tức thì trong môi trường tạm thời, tài liệu cung cấp hai bảng dữ liệu mẫu giả lập kích thước nhỏ (DEMO_DS gồm 6 chỗ ở và DEMO_RV gồm 18 đánh giá). Toàn bộ các hiện tượng thực tế của snapshot lớn (đánh giá sau mốc công bố, trùng cặp mã và ngày, lệch cửa sổ 12 tháng) đều được tái lập hoàn chỉnh trong mẫu nhỏ này. Tuyệt đối không suy diễn các con số thống kê của mẫu nhỏ thành đặc tính của toàn bộ thị trường lưu trú Santiago.
Khởi tạo môi trường và dữ liệu dùng chung
Toàn bộ các ví dụ và lời giải trong tài liệu sử dụng thư viện pandas và numpy với cấu hình hành vi sao chép an toàn (Copy-on-Write):
python
import numpy as np
import pandas as pd
# Thiết lập mốc chụp công bố và mốc bắt đầu cửa sổ 12 tháng
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
# Bảng đánh giá mẫu giả lập (18 đánh giá, 2 cột listing_id và date)
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
# Bảng chỗ ở mẫu giả lập (6 chỗ ở, đầy đủ các cột dẫn xuất và ngày đánh giá cuối)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
print(
"Khởi tạo dữ liệu mẫu thành công: Chỗ ở =",
len(DEMO_DS),
", Đánh giá =",
len(DEMO_RV),
)Bảng 10.0: Cấu trúc dữ liệu mẫu giả lập của danh sách chỗ ở (DEMO_DS):
| id | name | number_of_reviews | number_of_reviews_ltm | last_review |
|---|---|---|---|---|
| 11 | Depto Lastarria | 7 | 4 | 2026-06-30 |
| 12 | Pieza Bellavista | 4 | 2 | 2026-07-01 |
| 13 | Loft Providencia | 4 | 1 | 2025-07-01 |
| 14 | Casa Ñuñoa | 2 | 2 | 2026-06-30 |
| 15 | Studio nuevo | 0 | 0 | NaT |
| 16 | Hab Centro | 1 | 0 | 2021-02-02 |
Bài 1: Điền khuyết có căn cứ cho biến đếm tần suất và so sánh với điểm đánh giá
Mục tiêu: Phân tích bản chất nghiệp vụ của biến đếm tần suất đánh giá trung bình mỗi tháng (reviews_per_month), thực hiện điền khuyết giá trị 0 có căn cứ logic vững chắc khi số lượt đánh giá bằng 0, bảo toàn ban đầu và không làm thay đổi bảng dữ liệu gốc. Đối chiếu với nguyên tắc không điền 0 cho biến đo lường chất lượng (điểm đánh giá chỗ ở).
Dữ liệu đầu vào: Bảng dữ liệu chỗ ở thử nghiệm ds_demo_rpm gồm 4 chỗ ở:
- Chỗ ở 1 và 3 chưa từng có lượt đánh giá nào (
number_of_reviews == 0), cộtreviews_per_monthđang nhận giá trịNaN. - Chỗ ở 2 có 5 lượt đánh giá với tần suất đã biết 1.45 đánh giá/tháng.
- Chỗ ở 4 có 2 lượt đánh giá nhưng cột
reviews_per_monthbị khuyết do lỗi trích xuất dữ liệu.
Thao tác yêu cầu:
- Xây dựng hàm tạo Series
rpmtừ cộtreviews_per_month, điền giá trị 0.0 cho các chỗ ở cónumber_of_reviewsbằng 0. - Giữ nguyên ban đầu của bảng, tuyệt đối không chỉnh sửa bảng dữ liệu đầu vào.
- Bảo toàn nguyên vẹn giá trị
NaNtại chỗ ở 4 (chỗ ở đã có đánh giá nhưng thiếu số liệu tần suất). - Phân tích nguyên nhân vì sao việc điền 0 là hợp lý đối với biến tần suất nhưng không được phép áp dụng đối với điểm đánh giá sao (review rating score).
Trường hợp biên cần chú ý: Phải giữ nguyên của đầu vào, kể cả khi chỉ mục không liên tục hoặc là dạng chuỗi ký tự.
Lời giải
Cách 1: Sử dụng phép gán trên bản sao chỉ mục điều kiện
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def dien_rpm_co_can_cu_buoc(ds: pd.DataFrame) -> pd.Series:
"""Điền 0 cho reviews_per_month khi number_of_reviews bằng 0 theo cách tuần tự.
Tham số:
ds: DataFrame chứa cột 'number_of_reviews' và 'reviews_per_month'.
Trả về:
Series kiểu số thực cùng chỉ mục với ds, đã được điền 0 có căn cứ logic.
"""
rpm = ds["reviews_per_month"].copy()
chua_co_danh_gia = ds["number_of_reviews"].eq(0)
rpm.loc[chua_co_danh_gia] = 0.0
return rpm
# Dữ liệu thử nghiệm minh họa
ds_demo_rpm = pd.DataFrame(
{
"id": [11, 12, 13, 14],
"number_of_reviews": [0, 5, 0, 2],
"reviews_per_month": [np.nan, 1.45, np.nan, np.nan],
},
index=["N1", "N2", "N3", "N4"],
)
rpm_ket_qua_c1 = dien_rpm_co_can_cu_buoc(ds_demo_rpm)
print("Kết quả điền khuyết Cách 1:")
print(rpm_ket_qua_c1)
# Kết thúc lời giảiGiải thích cách 1
Tạo bản sao của cột trước khi gán để giữ dữ liệu gốc. Mặt nạ chỉ chọn chỗ ở có số đánh giá bằng 0 và tốc độ đánh giá đang thiếu. Điền 0 ở đúng các dòng đó theo giả thiết của bài, không lan sang dòng đã có đánh giá hoặc mọi cột .
Kết quả cách 1
| reviews_per_month | |
|---|---|
| N1 | 0 |
| N2 | 1.45 |
| N3 | 0 |
| N4 |
Cách 2: Sử dụng phương thức mask vector hóa
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
ds_demo_rpm = pd.DataFrame(
{
"id": [11, 12, 13, 14],
"number_of_reviews": [0, 5, 0, 2],
"reviews_per_month": [np.nan, 1.45, np.nan, np.nan],
},
index=["N1", "N2", "N3", "N4"],
)
def dien_rpm_co_can_cu_buoc(ds: pd.DataFrame) -> pd.Series:
"""Điền 0 cho reviews_per_month khi number_of_reviews bằng 0 theo cách tuần tự.
Tham số:
ds: DataFrame chứa cột 'number_of_reviews' và 'reviews_per_month'.
Trả về:
Series kiểu số thực cùng chỉ mục với ds, đã được điền 0 có căn cứ logic.
"""
rpm = ds["reviews_per_month"].copy()
chua_co_danh_gia = ds["number_of_reviews"].eq(0)
rpm.loc[chua_co_danh_gia] = 0.0
return rpm
rpm_ket_qua_c1 = dien_rpm_co_can_cu_buoc(ds_demo_rpm)
# Bắt đầu lời giải
def dien_rpm_co_can_cu_mask(ds: pd.DataFrame) -> pd.Series:
"""Điền 0 cho reviews_per_month bằng phương thức mask vector hóa.
Tham số:
ds: DataFrame chứa cột 'number_of_reviews' và 'reviews_per_month'.
Trả về:
Series kiểu số thực đã điền 0 có căn cứ.
"""
return ds["reviews_per_month"].mask(ds["number_of_reviews"].eq(0), 0.0)
rpm_ket_qua_c2 = dien_rpm_co_can_cu_mask(ds_demo_rpm)
assert rpm_ket_qua_c1.equals(
rpm_ket_qua_c2
), "Hai phương pháp phải cho kết quả đồng nhất"
print("Kết quả điền khuyết Cách 2 (vector hóa): Khớp hoàn toàn")
# Kết thúc lời giảiGiải thích cách 2
mask thay giá trị ở nơi điều kiện là True và giữ các vị trí còn lại. Điều kiện được tính theo cùng của bảng. Cách viết tập trung quy tắc thay thế vào một biểu thức, nhưng vẫn cần giải thích vì sao giá trị 0 có ý nghĩa cho đúng nhóm dòng đang xét.
Kết quả cách 2
text
Kết quả điền khuyết Cách 2 (vector hóa): Khớp hoàn toànSo sánh cách dùng
Hãy chọn Cách 1 khi muốn gán trực tiếp giá trị theo từng điều kiện cụ thể trên cột đang xử lý cục bộ. Cách 2 phù hợp hơn khi muốn thể hiện quy tắc thay thế bằng phương thức hàm nhất quán và giữ nguyên vẹn mảng dữ liệu ban đầu.
Bài 2: Điền giá trị thiếu theo nhóm phân loại và cờ đánh dấu
Mục tiêu: Ước lượng các giá trị bị khuyết trong cột giá lưu trú (price) bằng giá trị trung vị của từng loại phòng (room_type), đồng thời tạo cờ boolean gia_da_dien để lưu vết nguồn gốc dữ liệu, đo lường và giải thích sự dịch chuyển của giá trị trung bình mẫu.
Dữ liệu đầu vào: Bảng dữ liệu chỗ ở ds_demo_gia gồm 5 hàng thuộc ba loại phòng Entire home/apt, Private room và Shared room. Hai chỗ ở bị thiếu giá trong các nhóm có sẵn dữ liệu giá.
Thao tác yêu cầu:
- Tạo Series
gia_uoc_luong: Giữ nguyên giá đã biết, điền mỗi ô thiếu bằng trung vị giá của các chỗ ở có cùngroom_type. - Tạo Series boolean
gia_da_dien: Mang giá trịTruetại các hàng có giá được điền vàFalsetại các hàng giữ giá gốc. - Tính toán và so sánh giá trị trung bình của giá đã biết so với giá trị trung bình sau khi điền.
- Nêu rõ lưu ý bắt buộc khi công bố báo cáo thống kê dựa trên dữ liệu đã qua điền khuyết.
Trường hợp biên cần chú ý: Nếu trong bảng tồn tại một loại phòng mà toàn bộ các hàng đều bị thiếu giá, trung vị của nhóm đó là NaN, Series kết quả phải giữ nguyên giá trị NaN thay vì phát sinh lỗi.
Lời giải
Cách 1: Tính từ điển trung vị và ánh xạ tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def dien_gia_nhom_vong_lap(ds: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
"""Điền trung vị giá theo từng loại phòng theo cách tiếp cận ánh xạ từ điển.
Tham số:
ds: DataFrame chứa cột 'price' (số thực) và 'room_type' (chuỗi phân loại).
Trả về:
Tuple gồm (gia_uoc_luong, gia_da_dien).
"""
trung_vi_nhom = ds.groupby("room_type")["price"].median()
gia_uoc = ds["price"].copy()
gia_da_dien = ds["price"].isna()
for room_type, median_val in trung_vi_nhom.items():
mat_na = gia_da_dien & (ds["room_type"] == room_type)
gia_uoc.loc[mat_na] = median_val
return gia_uoc, gia_da_dien
# Dữ liệu thử nghiệm minh họa
ds_demo_gia = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105],
"room_type": [
"Entire home/apt",
"Entire home/apt",
"Private room",
"Private room",
"Shared room",
],
"price": [60000.0, np.nan, 30000.0, np.nan, 20000.0],
}
)
gia_uoc_c1, co_dien_c1 = dien_gia_nhom_vong_lap(ds_demo_gia)
print("Trung bình giá đã biết ban đầu:", ds_demo_gia["price"].mean())
print("Trung bình giá sau khi điền khuyết:", gia_uoc_c1.mean())
# Kết thúc lời giảiGiải thích cách 1
Duyệt từng nhóm loại phòng, tính trung vị từ những giá đã có rồi chỉ điền vào ô thiếu trong nhóm. Giữ lại cả dấu kiểm giá vốn đã thiếu để phân biệt số quan sát với số đã ước lượng. Nhóm không có một giá hợp lệ không thể tự cung cấp trung vị.
Kết quả cách 1
| Kết quả |
|---|
| 40000 |
Cách 2: Sử dụng groupby kết hợp transform vector hóa
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
ds_demo_gia = pd.DataFrame(
{
"id": [101, 102, 103, 104, 105],
"room_type": [
"Entire home/apt",
"Entire home/apt",
"Private room",
"Private room",
"Shared room",
],
"price": [60000.0, np.nan, 30000.0, np.nan, 20000.0],
}
)
def dien_gia_nhom_vong_lap(ds: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
"""Điền trung vị giá theo từng loại phòng theo cách tiếp cận ánh xạ từ điển.
Tham số:
ds: DataFrame chứa cột 'price' (số thực) và 'room_type' (chuỗi phân loại).
Trả về:
Tuple gồm (gia_uoc_luong, gia_da_dien).
"""
trung_vi_nhom = ds.groupby("room_type")["price"].median()
gia_uoc = ds["price"].copy()
gia_da_dien = ds["price"].isna()
for room_type, median_val in trung_vi_nhom.items():
mat_na = gia_da_dien & (ds["room_type"] == room_type)
gia_uoc.loc[mat_na] = median_val
return gia_uoc, gia_da_dien
gia_uoc_c1, co_dien_c1 = dien_gia_nhom_vong_lap(ds_demo_gia)
# Bắt đầu lời giải
def dien_gia_nhom_transform(ds: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
"""Điền trung vị giá theo loại phòng bằng transform vector hóa.
Tham số:
ds: DataFrame chứa cột 'price' và 'room_type'.
Trả về:
Tuple gồm (gia_uoc_luong, gia_da_dien).
"""
trung_vi_theo_hang = ds.groupby("room_type")["price"].transform("median")
gia_uoc = ds["price"].fillna(trung_vi_theo_hang)
gia_da_dien = ds["price"].isna()
return gia_uoc, gia_da_dien
gia_uoc_c2, co_dien_c2 = dien_gia_nhom_transform(ds_demo_gia)
assert gia_uoc_c1.equals(gia_uoc_c2) and co_dien_c1.equals(
co_dien_c2
), "Hai phương pháp phải đồng nhất"
print("Xác nhận Cách 2: Vector hóa thành công")
# Kết thúc lời giảiGiải thích cách 2
groupby(...).transform("median") trả một trung vị tương ứng với từng dòng nguồn. fillna hoặc mặt nạ sau đó chỉ thay ô thiếu, giữ giá đã quan sát. transform có cùng với bảng, thuận tiện hơn việc ghép một bảng tổng hợp nhỏ trở lại từng dòng.
Kết quả cách 2
text
Xác nhận Cách 2: Vector hóa thành côngSo sánh cách dùng
Bạn nên chọn Cách 1 khi muốn xem trước bảng trung vị từng nhóm hoặc cần lưu từ điển quy tắc để áp dụng cho dữ liệu mới. Cách 2 thuận tiện hơn khi muốn điền giá trị đại diện trực tiếp trên bảng nguồn nhờ khả năng giữ nguyên của transform.
Bài 3: Quy tắc kiểm tra tính hợp lệ của giá với bảng cờ boolean
Mục tiêu: Xây dựng hàm kiểm định tính hợp lệ của cột giá phòng, tạo lập gồm ba cột cờ boolean độc lập: thieu, khong_duong, cao. Phân biệt rạch ròi giữa giá trị bị khuyết và giá trị vi phạm biên nghiệp vụ (float64 lẫn kiểu số thực cho phép khuyết Float64.
Dữ liệu đầu vào: Bảng dữ liệu kiểm thử gia_thu gồm 6 hàng với các giá trị: np.nan, 0.0, -5000.0, 100000.0, 150000.0, 250000.0, không liên tục ["P6", "P2", "P9", "P1", "P5", "P3"], cùng ngưỡng kiểm tra giá cao là 120,000.0 CLP.
Thao tác yêu cầu:
- Cột
thieu: Đánh dấuTruenếu giá trị bị khuyết (NaNhoặc<NA>). - Cột
khong_duong: Đánh dấuTruenếu giá có tồn tại nhưng có giá trị. - Cột
cao: Đánh dấuTruenếu giá có tồn tại và lớn hơn ngưỡng chỉ định. - Bảo đảm các cột cờ có kiểu dữ liệu boolean chuẩn (
bool), không chứa bất kỳ nào, và giữ nguyên của bảng đầu vào.
Trường hợp biên cần chú ý: Tại các hàng có giá trị bị khuyết, cờ khong_duong và cao bắt buộc phải nhận giá trị False, tuyệt đối không được để giá trị khuyết lan truyền sang kết quả cờ.
Lời giải
Cách 1: Xây dựng từng cột cờ bằng biểu thức điều kiện tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def kiem_gia_tuan_tu(d: pd.DataFrame, nguong: float) -> pd.DataFrame:
"""Kiểm tra tính hợp lệ của giá và trả về ba cột cờ boolean tuần tự.
Tham số:
d: DataFrame chứa cột 'price'.
nguong: Giá trị ngưỡng trần (float).
Trả về:
DataFrame gồm ba cột boolean: thieu, khong_duong, cao.
"""
gia = d["price"]
co_thieu = gia.isna()
co_khong_duong = gia.notna() & (gia <= 0)
co_cao = gia.notna() & (gia > nguong)
ket_qua = pd.DataFrame(
{
"thieu": co_thieu.astype(bool),
"khong_duong": co_khong_duong.fillna(False).astype(bool),
"cao": co_cao.fillna(False).astype(bool),
},
index=d.index,
)
return ket_qua
# Bảng thử nghiệm kiểm tra tính hợp lệ
gia_thu = pd.DataFrame(
{
"price": [np.nan, 0.0, -5000.0, 100000.0, 150000.0, 250000.0],
},
index=["P6", "P2", "P9", "P1", "P5", "P3"],
)
co_gia_c1 = kiem_gia_tuan_tu(gia_thu, 120000.0)
print("Kết quả bảng cờ kiểm tra Cách 1:")
print(co_gia_c1)
# Kết thúc lời giảiGiải thích cách 1
Xây từng cờ kiểm theo điều kiện giá thiếu, không dương hoặc vượt ngưỡng. Các cờ có thể chồng nhau, nên tổng số cờ không nhất thiết bằng số dòng lỗi phân biệt. Trả bảng cờ giữ để có thể đối chiếu chính xác bản ghi gốc.
Kết quả cách 1
| thieu | khong_duong | cao | |
|---|---|---|---|
| P6 | True | False | False |
| P2 | False | True | False |
| P9 | False | True | False |
| P1 | False | False | False |
| P5 | False | False | True |
| P3 | False | False | True |
Cách 2: Đóng gói từ điển vector hóa tương thích kiểu nullable
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
gia_thu = pd.DataFrame(
{
"price": [np.nan, 0.0, -5000.0, 100000.0, 150000.0, 250000.0],
},
index=["P6", "P2", "P9", "P1", "P5", "P3"],
)
def kiem_gia_tuan_tu(d: pd.DataFrame, nguong: float) -> pd.DataFrame:
"""Kiểm tra tính hợp lệ của giá và trả về ba cột cờ boolean tuần tự.
Tham số:
d: DataFrame chứa cột 'price'.
nguong: Giá trị ngưỡng trần (float).
Trả về:
DataFrame gồm ba cột boolean: thieu, khong_duong, cao.
"""
gia = d["price"]
co_thieu = gia.isna()
co_khong_duong = gia.notna() & (gia <= 0)
co_cao = gia.notna() & (gia > nguong)
ket_qua = pd.DataFrame(
{
"thieu": co_thieu.astype(bool),
"khong_duong": co_khong_duong.fillna(False).astype(bool),
"cao": co_cao.fillna(False).astype(bool),
},
index=d.index,
)
return ket_qua
co_gia_c1 = kiem_gia_tuan_tu(gia_thu, 120000.0)
# Bắt đầu lời giải
def kiem_gia(d: pd.DataFrame, nguong: float) -> pd.DataFrame:
"""Hàm kiểm tra giá chuẩn mực trả về ba cờ boolean vector hóa.
Tham số:
d: DataFrame chứa cột 'price' (float64 hoặc Float64).
nguong: Ngưỡng giá xem xét.
Trả về:
DataFrame gồm ba cột cờ thieu, khong_duong, cao.
"""
p = d["price"]
hop_le = p.notna()
return pd.DataFrame(
{
"thieu": ~hop_le,
"khong_duong": (hop_le & (p <= 0)).fillna(False).astype(bool),
"cao": (hop_le & (p > nguong)).fillna(False).astype(bool),
},
index=d.index,
)
co_gia_c2 = kiem_gia(gia_thu, 120000.0)
assert co_gia_c1.equals(co_gia_c2), "Kết quả hai cách phải hoàn toàn đồng nhất"
print("Xác nhận Cách 2: Tương thích hoàn toàn")
# Kết thúc lời giảiGiải thích cách 2
Biểu thức boolean theo cột thể hiện cùng bộ quy tắc cho toàn bảng. Tên cột cờ nên mô tả điều đã kiểm, thay vì gọi mọi dòng là chắc chắn sai. Ngưỡng là lựa chọn kiểm tra cần ghi lại, không phải bằng chứng tự động rằng giá cao phải bị xóa.
Kết quả cách 2
text
Xác nhận Cách 2: Tương thích hoàn toànSo sánh cách dùng
Hãy chọn Cách 1 khi cần kiểm tra từng điều kiện riêng lẻ và theo dõi số bản ghi vi phạm sau mỗi phép so sánh. Cách 2 phù hợp khi bạn muốn gom các tiêu chí vào một từ điển tập trung để dễ bảo trì và xuất bảng kiểm toán.
Bài 4: Phân vị ngoại lai, loại bỏ hàng và chặn giá Winsorization
Mục tiêu: Tính toán hai ngưỡng sàng lọc ngoại lai kinh điển: Ngưỡng khoảng tứ phân vị IQR (so_sanh đối chiếu tác động của ba chiến lược xử lý: Giữ nguyên giá dương, loại bỏ hàng vượt P99, và chặn giá tại P99 (Winsorization) lên số lượng quan sát, giá trị trung bình và giá trị trung vị.
Dữ liệu đầu vào: Bảng dữ liệu chỗ ở ds_demo_phan_vi gồm 12 hàng với các mức giá dương, giá khuyết và giá âm.
Thao tác yêu cầu:
- Lọc tập hợp các giá trị dương đã biết (
). - Xác định các phân vị
, , khoảng tứ phân vị $ ext{IQR} = Q_3 - Q_1$, và ngưỡng trên $ ext{nguong_iqr} = Q_3 + 1.5 imes ext{IQR}$. Đếm số giá trị vượt ngưỡng IQR. - Tính ngưỡng phân vị 99% $ ext{nguong_p99} = p. ext{quantile}(0.99)$.
- Lập bảng
so_sanhgồm 3 hàng (co_gia_duong,bo_tren_p99,chan_p99) và 3 cột (so_cho_o,trung_binh,trung_vi). - Phân tích sự đánh đổi giữa việc loại bỏ hàng quan sát so với việc can thiệp chặn giá.
Trường hợp biên cần chú ý: Mọi phép tính ngưỡng và bắt buộc chỉ thực hiện trên tập giá dương đã biết, tuyệt đối không tính giá khuyết hoặc giá
Lời giải
Cách 1: Lọc dữ liệu tuần tự và tổng hợp thủ công
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def phan_vi_chan_gia_buoc(ds: pd.DataFrame) -> tuple[float, int, float, pd.DataFrame]:
"""Tính ngưỡng ngoại lai và lập bảng so sánh ba chiến lược theo cách tuần tự.
Tham số:
ds: DataFrame chứa cột 'price'.
Trả về:
Tuple gồm (nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh).
"""
gia_duong = ds.loc[ds["price"] > 0, "price"].dropna()
q1 = gia_duong.quantile(0.25)
q3 = gia_duong.quantile(0.75)
iqr = q3 - q1
nguong_iqr = float(q3 + 1.5 * iqr)
so_vuot_iqr = int((gia_duong > nguong_iqr).sum())
nguong_p99 = float(gia_duong.quantile(0.99))
# 1. Giữ nguyên giá dương
n_duong = len(gia_duong)
tb_duong = float(gia_duong.mean())
tv_duong = float(gia_duong.median())
# 2. Bỏ giá trên P99
gia_bo = gia_duong[gia_duong <= nguong_p99]
n_bo = len(gia_bo)
tb_bo = float(gia_bo.mean())
tv_bo = float(gia_bo.median())
# 3. Chặn giá tại P99
gia_chan = gia_duong.copy()
gia_chan[gia_chan > nguong_p99] = nguong_p99
n_chan = len(gia_chan)
tb_chan = float(gia_chan.mean())
tv_chan = float(gia_chan.median())
so_sanh = pd.DataFrame(
{
"so_cho_o": [n_duong, n_bo, n_chan],
"trung_binh": [tb_duong, tb_bo, tb_chan],
"trung_vi": [tv_duong, tv_bo, tv_chan],
},
index=["co_gia_duong", "bo_tren_p99", "chan_p99"],
)
return nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh
# Dữ liệu thử nghiệm minh họa
ds_demo_phan_vi = pd.DataFrame(
{
"price": [
10000.0,
20000.0,
30000.0,
40000.0,
50000.0,
60000.0,
70000.0,
80000.0,
90000.0,
500000.0,
np.nan,
-100.0,
],
}
)
iqr_c1, n_iqr_c1, p99_c1, bang_c1 = phan_vi_chan_gia_buoc(ds_demo_phan_vi)
print(f"Ngưỡng IQR: {iqr_c1:.2f}, Số vượt: {n_iqr_c1}, Ngưỡng P99: {p99_c1:.2f}")
print(bang_c1.round(2))
# Kết thúc lời giảiGiải thích cách 1
Tính P99 từ phần giá hợp lệ trước khi đếm dòng vượt ngưỡng. Tạo hai kết quả riêng cho bỏ hàng và chặn giá vì chúng thay đổi dữ liệu theo hai cách khác nhau. Giữ ngưỡng cùng số dòng bị tác động để người đọc đánh giá mức thay đổi.
Kết quả cách 1
| so_cho_o | trung_binh | trung_vi | |
|---|---|---|---|
| co_gia_duong | 10 | 95000 | 55000 |
| bo_tren_p99 | 9 | 50000 | 50000 |
| chan_p99 | 10 | 91310 | 55000 |
Cách 2: Sử dụng phương thức clip kết hợp tổng hợp vector hóa
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
ds_demo_phan_vi = pd.DataFrame(
{
"price": [
10000.0,
20000.0,
30000.0,
40000.0,
50000.0,
60000.0,
70000.0,
80000.0,
90000.0,
500000.0,
np.nan,
-100.0,
],
}
)
def phan_vi_chan_gia_buoc(ds: pd.DataFrame) -> tuple[float, int, float, pd.DataFrame]:
"""Tính ngưỡng ngoại lai và lập bảng so sánh ba chiến lược theo cách tuần tự.
Tham số:
ds: DataFrame chứa cột 'price'.
Trả về:
Tuple gồm (nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh).
"""
gia_duong = ds.loc[ds["price"] > 0, "price"].dropna()
q1 = gia_duong.quantile(0.25)
q3 = gia_duong.quantile(0.75)
iqr = q3 - q1
nguong_iqr = float(q3 + 1.5 * iqr)
so_vuot_iqr = int((gia_duong > nguong_iqr).sum())
nguong_p99 = float(gia_duong.quantile(0.99))
# 1. Giữ nguyên giá dương
n_duong = len(gia_duong)
tb_duong = float(gia_duong.mean())
tv_duong = float(gia_duong.median())
# 2. Bỏ giá trên P99
gia_bo = gia_duong[gia_duong <= nguong_p99]
n_bo = len(gia_bo)
tb_bo = float(gia_bo.mean())
tv_bo = float(gia_bo.median())
# 3. Chặn giá tại P99
gia_chan = gia_duong.copy()
gia_chan[gia_chan > nguong_p99] = nguong_p99
n_chan = len(gia_chan)
tb_chan = float(gia_chan.mean())
tv_chan = float(gia_chan.median())
so_sanh = pd.DataFrame(
{
"so_cho_o": [n_duong, n_bo, n_chan],
"trung_binh": [tb_duong, tb_bo, tb_chan],
"trung_vi": [tv_duong, tv_bo, tv_chan],
},
index=["co_gia_duong", "bo_tren_p99", "chan_p99"],
)
return nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh
iqr_c1, n_iqr_c1, p99_c1, bang_c1 = phan_vi_chan_gia_buoc(ds_demo_phan_vi)
# Bắt đầu lời giải
def phan_vi_chan_gia_clip(ds: pd.DataFrame) -> tuple[float, int, float, pd.DataFrame]:
"""Tính ngưỡng ngoại lai và lập bảng so sánh bằng clip vector hóa.
Tham số:
ds: DataFrame chứa cột 'price'.
Trả về:
Tuple gồm (nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh).
"""
p = ds.loc[ds["price"] > 0, "price"].dropna()
q1, q3 = p.quantile(0.25), p.quantile(0.75)
nguong_iqr = float(q3 + 1.5 * (q3 - q1))
so_vuot_iqr = int((p > nguong_iqr).sum())
nguong_p99 = float(p.quantile(0.99))
p_bo = p[p <= nguong_p99]
p_chan = p.clip(upper=nguong_p99)
du_lieu_so_sanh = {
"co_gia_duong": (len(p), float(p.mean()), float(p.median())),
"bo_tren_p99": (len(p_bo), float(p_bo.mean()), float(p_bo.median())),
"chan_p99": (len(p_chan), float(p_chan.mean()), float(p_chan.median())),
}
so_sanh = pd.DataFrame.from_dict(
du_lieu_so_sanh, orient="index", columns=["so_cho_o", "trung_binh", "trung_vi"]
)
return nguong_iqr, so_vuot_iqr, nguong_p99, so_sanh
iqr_c2, n_iqr_c2, p99_c2, bang_c2 = phan_vi_chan_gia_clip(ds_demo_phan_vi)
pd.testing.assert_frame_equal(bang_c1, bang_c2)
print("Xác nhận Cách 2: Kết quả khớp tuyệt đối")
# Kết thúc lời giảiGiải thích cách 2
clip(upper=nguong) thay giá vượt ngưỡng bằng ngưỡng mà giữ số dòng. Lọc bằng mặt nạ lại làm giảm cỡ mẫu. Hai kết quả không thể xem là cùng một dữ liệu sau làm sạch, và không có quy tắc thống kê nào tự cho phép chặn mọi giá cao trong mọi bài toán.
Kết quả cách 2
text
Xác nhận Cách 2: Kết quả khớp tuyệt đốiSo sánh cách dùng
Cách 1 đặt ngưỡng và các phương án xử lý cạnh nhau để thấy số dòng, trung bình và trung vị thay đổi thế nào. Cách 2 mở rõ thao tác chặn bằng clip. Giá vượt P99 chỉ là dấu hiệu cần xem xét, chưa đủ để kết luận đó là lỗi.
Bài 5: Phân biệt bản ghi nhập lặp và các sự kiện trùng thuộc tính
Mục tiêu: Phân biệt ranh giới kỹ thuật giữa bản ghi nhập lặp (khi có khóa định danh duy nhất của đánh giá review_id) với hiện tượng trùng thuộc tính trong bảng rút gọn chỉ có cặp (listing_id, date). Thực hiện khử trùng an toàn khi có khóa riêng và đếm số lượng dòng trùng lặp trong bảng rút gọn.
Dữ liệu đầu vào:
- Bảng mẫu đầy đủ
mau_rvgồm 4 dòng, có khóa định danh riêngreview_id, trong đó mã R1 xuất hiện hai lần với cùng mã chỗ ở, ngày và nội dung bình luận. - Bảng rút gọn
rv_rut_gongồm 5 dòng, chỉ lưu hai thuộc tínhlisting_idvàdate.
Thao tác yêu cầu:
- Trên
mau_rv: Khử trùng bản nhập lặp bằngdrop_duplicates(subset=["review_id"]), tạo bảngrv_khong_lapgiữ nguyên 3 đánh giá phân biệt. - Trên
rv_rut_gon: Tính toánso_lap_sau_dau(số dòng lặp lại sau lần xuất hiện đầu tiên) vàso_hang_nhom_lap(tổng số dòng thuộc tất cả các nhóm có sự lặp lại). - Giải thích lý do vì sao chưa thể kết luận các dòng trùng cặp
(listing_id, date)là bản ghi lỗi trong tập dữ liệu rút gọn.
Trường hợp biên cần chú ý: Phải giữ nguyên vẹn bảng đầu vào, không xóa hàng của bảng rút gọn khi chưa có khóa định danh duy nhất.
Lời giải
Cách 1: Sử dụng tham số keep trong duplicated và drop_duplicates
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
def xu_ly_trung_lap_co_ban(
mau_rv: pd.DataFrame, rv_rut_gon: pd.DataFrame
) -> tuple[pd.DataFrame, int, int]:
"""Khử trùng trên khóa riêng và đếm số lượng dòng trùng theo cặp thuộc tính.
Tham số:
mau_rv: DataFrame có cột 'review_id'.
rv_rut_gon: DataFrame có cột 'listing_id' và 'date'.
Trả về:
Tuple gồm (rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap).
"""
rv_khong_lap = mau_rv.drop_duplicates(subset=["review_id"]).copy()
cap = ["listing_id", "date"]
so_lap_sau_dau = int(rv_rut_gon.duplicated(subset=cap, keep="first").sum())
so_hang_nhom_lap = int(rv_rut_gon.duplicated(subset=cap, keep=False).sum())
return rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap
# Dữ liệu thử nghiệm minh họa
mau_rv_demo = pd.DataFrame(
{
"review_id": ["R1", "R2", "R1", "R3"],
"listing_id": ["P1", "P1", "P1", "P2"],
"date": pd.to_datetime(["2026-06-28"] * 4),
"noi_dung": ["Yên tĩnh", "Sạch sẽ", "Yên tĩnh", "Thuận tiện"],
}
)
rv_rut_gon_demo = pd.DataFrame(
{
"listing_id": [11, 11, 12, 12, 12],
"date": pd.to_datetime(
["2025-08-10", "2025-08-10", "2026-01-05", "2026-01-05", "2026-01-05"]
),
}
)
rv_sach_c1, lap_sau_c1, nhom_lap_c1 = xu_ly_trung_lap_co_ban(
mau_rv_demo, rv_rut_gon_demo
)
print("Số đánh giá sau khử trùng khóa riêng:", len(rv_sach_c1))
print(f"Số lặp sau lần đầu: {lap_sau_c1}, Tổng số hàng thuộc nhóm lặp: {nhom_lap_c1}")
# Kết thúc lời giảiGiải thích cách 1
duplicated tìm các dòng trùng theo những cột đã chọn, còn drop_duplicates thực hiện một quyết định loại dòng. Có mã review phân biệt thì có thể kiểm bản nhập lặp theo mã đó. Chỉ có listing_id và ngày chưa đủ phân biệt hai người cùng đánh giá trong một ngày.
Kết quả cách 1
| Kết quả |
|---|
| 3 |
Cách 2: Sử dụng groupby để phân tích kích thước nhóm lặp
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
mau_rv_demo = pd.DataFrame(
{
"review_id": ["R1", "R2", "R1", "R3"],
"listing_id": ["P1", "P1", "P1", "P2"],
"date": pd.to_datetime(["2026-06-28"] * 4),
"noi_dung": ["Yên tĩnh", "Sạch sẽ", "Yên tĩnh", "Thuận tiện"],
}
)
rv_rut_gon_demo = pd.DataFrame(
{
"listing_id": [11, 11, 12, 12, 12],
"date": pd.to_datetime(
["2025-08-10", "2025-08-10", "2026-01-05", "2026-01-05", "2026-01-05"]
),
}
)
def xu_ly_trung_lap_co_ban(
mau_rv: pd.DataFrame, rv_rut_gon: pd.DataFrame
) -> tuple[pd.DataFrame, int, int]:
"""Khử trùng trên khóa riêng và đếm số lượng dòng trùng theo cặp thuộc tính.
Tham số:
mau_rv: DataFrame có cột 'review_id'.
rv_rut_gon: DataFrame có cột 'listing_id' và 'date'.
Trả về:
Tuple gồm (rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap).
"""
rv_khong_lap = mau_rv.drop_duplicates(subset=["review_id"]).copy()
cap = ["listing_id", "date"]
so_lap_sau_dau = int(rv_rut_gon.duplicated(subset=cap, keep="first").sum())
so_hang_nhom_lap = int(rv_rut_gon.duplicated(subset=cap, keep=False).sum())
return rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap
rv_sach_c1, lap_sau_c1, nhom_lap_c1 = xu_ly_trung_lap_co_ban(
mau_rv_demo, rv_rut_gon_demo
)
# Bắt đầu lời giải
def xu_ly_trung_lap_nang_cao(
mau_rv: pd.DataFrame, rv_rut_gon: pd.DataFrame
) -> tuple[pd.DataFrame, int, int]:
"""Phân tích trùng lặp bằng kỹ thuật nhóm groupby.
Tham số:
mau_rv: DataFrame có cột 'review_id'.
rv_rut_gon: DataFrame có hai cột 'listing_id' và 'date'.
Trả về:
Tuple gồm (rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap).
"""
rv_khong_lap = mau_rv.loc[~mau_rv["review_id"].duplicated()].copy()
tan_suat = rv_rut_gon.groupby(["listing_id", "date"]).size()
nhom_bi_lap = tan_suat[tan_suat > 1]
so_hang_nhom_lap = int(nhom_bi_lap.sum())
so_lap_sau_dau = int((nhom_bi_lap - 1).sum())
return rv_khong_lap, so_lap_sau_dau, so_hang_nhom_lap
rv_sach_c2, lap_sau_c2, nhom_lap_c2 = xu_ly_trung_lap_nang_cao(
mau_rv_demo, rv_rut_gon_demo
)
assert (
lap_sau_c1 == lap_sau_c2 and nhom_lap_c1 == nhom_lap_c2
), "Kết quả hai cách phải đồng nhất"
print("Xác nhận Cách 2: Phân tích kích thước nhóm hoàn tất")
# Kết thúc lời giảiGiải thích cách 2
Tách phép đếm trùng khỏi phép loại dòng để báo cáo được cả hiện tượng lẫn quyết định. keep=False đánh dấu mọi thành viên của nhóm trùng, còn keep="first" giữ một dòng đại diện. Kết quả phải ghi rõ đang đếm dòng tham gia nhóm hay số dòng vượt quá một đại diện.
Kết quả cách 2
text
Xác nhận Cách 2: Phân tích kích thước nhóm hoàn tấtSo sánh cách dùng
Cách 1 thể hiện trực tiếp thao tác nhận diện và xử lý mã review bị lặp. Cách 2 giúp xem quy mô các nhóm lặp trước khi quyết định giữ dòng nào. Không dùng sự trùng ngày và mã chỗ ở làm bằng chứng duy nhất rằng hai review là một.
Bài 6: Kiểm tra miền thời gian và bảng cờ kiểm tra đánh giá
Mục tiêu: Kiểm tra miền thời gian của bảng đánh giá (time_domain), xác định ngày sớm nhất, ngày muộn nhất và số lượng đánh giá rơi vào sau mốc công bố. Xây dựng bảng cờ kiểm tra chi tiết (kiem_reviews) cho từng dòng đánh giá theo các quy tắc kiểm tra ngày khuyết, ngày sau mốc và mã chỗ ở không khớp, sau đó áp dụng phép kiểm tra tổng hợp any(axis=1).
Dữ liệu đầu vào: Bảng đánh giá mẫu DEMO_RV (18 dòng), danh sách mã chỗ ở hợp lệ từ DEMO_DS["id"], và mốc chụp công bố MOC = "2026-06-29".
Thao tác yêu cầu:
- Viết hàm
time_domain(rv: pd.DataFrame, moc: str) -> dicttrả về từ điển đúng ba khóa:ngay_dau(Timestamp),ngay_cuoi(Timestamp), vàso_sau_moc(int). - Viết hàm
kiem_reviews(r: pd.DataFrame, ids, moc: str) -> pd.DataFrametrả về 3 cột boolean:ngay_thieu,sau_moc,id_khong_khop. - Tính toán tổng số lượt gắn cờ và số dòng vi phạm ít nhất một cờ bằng phương thức
any(axis=1). - Phân tích chính sách xử lý đối với các đánh giá sau mốc chụp và nguyên tắc không tùy tiện thay đổi mốc công bố.
Trường hợp biên cần chú ý: Các đánh giá có ngày đúng bằng mốc chụp công bố (date == moc) vẫn thuộc phạm vi hợp lệ, cờ sau_moc bắt buộc nhận giá trị False. Khi ngày bị khuyết, cờ sau_moc phải là False.
Lời giải
Cách 1: Kiểm tra miền và cờ tuần tự từng bước
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
# Bắt đầu lời giải
def time_domain_buoc(rv: pd.DataFrame, moc: str) -> dict:
"""Kiểm tra miền thời gian của bảng đánh giá tuần tự.
Tham số:
rv: DataFrame chứa cột 'date' kiểu datetime64.
moc: Chuỗi ngày mốc định dạng 'YYYY-MM-DD'.
Trả về:
Dict gồm ngay_dau, ngay_cuoi, so_sau_moc.
"""
ngay = rv["date"].dropna()
moc_ts = pd.Timestamp(moc)
return {
"ngay_dau": ngay.min(),
"ngay_cuoi": ngay.max(),
"so_sau_moc": int((ngay > moc_ts).sum()),
}
def kiem_reviews_buoc(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ kiểm tra đánh giá theo từng bước.
Tham số:
r: DataFrame chứa 'listing_id' và 'date'.
ids: Tập hợp hoặc Series các mã chỗ ở hợp lệ.
moc: Mốc chụp công bố.
Trả về:
DataFrame gồm ba cột cờ boolean: ngay_thieu, sau_moc, id_khong_khop.
"""
moc_ts = pd.Timestamp(moc)
co_ngay_thieu = r["date"].isna()
co_sau_moc = r["date"].notna() & (r["date"] > moc_ts)
co_id_khong_khop = ~r["listing_id"].isin(ids)
return pd.DataFrame(
{
"ngay_thieu": co_ngay_thieu.astype(bool),
"sau_moc": co_sau_moc.astype(bool),
"id_khong_khop": co_id_khong_khop.astype(bool),
},
index=r.index,
)
mien_c1 = time_domain_buoc(DEMO_RV, MOC)
co_rv_c1 = kiem_reviews_buoc(DEMO_RV, DEMO_DS["id"], MOC)
print("Miền thời gian Cách 1:", mien_c1)
print("Tổng lượt gắn cờ:", co_rv_c1.sum().to_dict())
print("Số dòng có ít nhất một cờ:", int(co_rv_c1.any(axis=1).sum()))
# Kết thúc lời giảiGiải thích cách 1
Chuyển mốc so sánh thành Timestamp, rồi tính ngày nhỏ nhất, lớn nhất và số ngày vượt mốc. Phép > không gồm chính ngày mốc theo yêu cầu. Trả ba chỉ số có tên để không nhầm số dòng sau mốc với độ dài khoảng thời gian.
Kết quả cách 1
| Trường | Kết quả |
|---|---|
| ngay_thieu | 0 |
| sau_moc | 3 |
| id_khong_khop | 0 |
Cách 2: Đóng gói kiểm tra vector hóa kết hợp isin
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def time_domain_buoc(rv: pd.DataFrame, moc: str) -> dict:
"""Kiểm tra miền thời gian của bảng đánh giá tuần tự.
Tham số:
rv: DataFrame chứa cột 'date' kiểu datetime64.
moc: Chuỗi ngày mốc định dạng 'YYYY-MM-DD'.
Trả về:
Dict gồm ngay_dau, ngay_cuoi, so_sau_moc.
"""
ngay = rv["date"].dropna()
moc_ts = pd.Timestamp(moc)
return {
"ngay_dau": ngay.min(),
"ngay_cuoi": ngay.max(),
"so_sau_moc": int((ngay > moc_ts).sum()),
}
def kiem_reviews_buoc(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ kiểm tra đánh giá theo từng bước.
Tham số:
r: DataFrame chứa 'listing_id' và 'date'.
ids: Tập hợp hoặc Series các mã chỗ ở hợp lệ.
moc: Mốc chụp công bố.
Trả về:
DataFrame gồm ba cột cờ boolean: ngay_thieu, sau_moc, id_khong_khop.
"""
moc_ts = pd.Timestamp(moc)
co_ngay_thieu = r["date"].isna()
co_sau_moc = r["date"].notna() & (r["date"] > moc_ts)
co_id_khong_khop = ~r["listing_id"].isin(ids)
return pd.DataFrame(
{
"ngay_thieu": co_ngay_thieu.astype(bool),
"sau_moc": co_sau_moc.astype(bool),
"id_khong_khop": co_id_khong_khop.astype(bool),
},
index=r.index,
)
mien_c1 = time_domain_buoc(DEMO_RV, MOC)
co_rv_c1 = kiem_reviews_buoc(DEMO_RV, DEMO_DS["id"], MOC)
# Bắt đầu lời giải
def time_domain(rv: pd.DataFrame, moc: str) -> dict:
"""Kiểm tra miền thời gian của bảng đánh giá chuẩn mực.
Tham số:
rv: DataFrame chứa cột 'date'.
moc: Mốc chụp công bố (chuỗi 'YYYY-MM-DD').
Trả về:
Dict có đúng ba khóa ngay_dau, ngay_cuoi, so_sau_moc.
"""
d = rv["date"]
return {
"ngay_dau": d.min(),
"ngay_cuoi": d.max(),
"so_sau_moc": int((d > pd.Timestamp(moc)).sum()),
}
def kiem_reviews(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ boolean kiểm tra đánh giá vector hóa.
Tham số:
r: DataFrame có 'listing_id' và 'date'.
ids: Danh sách mã chỗ ở hợp lệ.
moc: Mốc công bố.
Trả về:
DataFrame gồm ba cột cờ boolean.
"""
d = r["date"]
moc_ts = pd.Timestamp(moc)
return pd.DataFrame(
{
"ngay_thieu": d.isna(),
"sau_moc": (d.notna() & (d > moc_ts)).fillna(False).astype(bool),
"id_khong_khop": (~r["listing_id"].isin(ids)).astype(bool),
},
index=r.index,
)
mien_c2 = time_domain(DEMO_RV, MOC)
co_rv_c2 = kiem_reviews(DEMO_RV, DEMO_DS["id"], MOC)
assert mien_c1 == mien_c2 and co_rv_c1.equals(
co_rv_c2
), "Kết quả hai cách phải đồng nhất"
print("Xác nhận Cách 2: Kiểm tra miền thời gian và cờ hoàn tất")
# Kết thúc lời giảiGiải thích cách 2
Các phép tổng hợp trên Series ngày tránh duyệt từng dòng trong Python. Cột ngày phải được đọc đúng trước khi gọi hàm. Ngày vượt mốc cần gắn cờ để đối chiếu phạm vi thu thập, không được tự suy ra nguyên nhân hoặc lặng lẽ sửa ngày.
Kết quả cách 2
text
Xác nhận Cách 2: Kiểm tra miền thời gian và cờ hoàn tấtSo sánh cách dùng
Bạn nên chọn Cách 1 khi làm việc tương tác để xem nhanh mốc thời gian biên và kiểm tra sơ bộ độ bao phủ dữ liệu. Cách 2 phù hợp khi cần đóng gói các quy tắc kiểm tra vào một hàm dùng chung để tự động hóa khâu tiền xử lý.
Bài 7: Kiểm tra toàn vẹn khóa ngoại và đánh giá mồ côi
Mục tiêu: Kiểm tra tính toàn vẹn tham chiếu khóa ngoại giữa bảng đánh giá (reviews.listing_id) và bảng chỗ ở (listings.id). Phát hiện các dòng đánh giá mồ côi (orphan reviews) không thể liên kết với bất kỳ chỗ ở hợp lệ nào.
Dữ liệu đầu vào: Bảng đánh giá mẫu DEMO_RV (18 dòng), bảng chỗ ở DEMO_DS (6 chỗ ở), và bảng chỗ ở giả định bị thiếu chỗ ở mã 11 ds_thieu = DEMO_DS[DEMO_DS["id"] != 11].
Thao tác yêu cầu:
- Viết hàm
orphan_reviews(rv: pd.DataFrame, ds: pd.DataFrame) -> intđếm số dòng đánh giá cólisting_idkhông tồn tại trongds["id"]. - Kiểm thử hàm trên dữ liệu chuẩn và trên bảng chỗ ở bị khuyết để xác nhận mọi đánh giá mồ côi đều được đếm đủ.
- Giải thích lý do vì sao một kết quả kiểm tra bằng 0 vi phạm vẫn bắt buộc phải được ghi nhận trang trọng vào báo cáo QA.
Trường hợp biên cần chú ý: Nếu cùng một mã chỗ ở mồ côi xuất hiện nhiều lần trong bảng đánh giá, mỗi dòng vi phạm đều phải được đếm riêng rẽ, không dùng nunique.
Lời giải
Cách 1: Sử dụng toán tử đảo ngược trên phương thức isin
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
# Bắt đầu lời giải
def orphan_reviews_isin(rv: pd.DataFrame, ds: pd.DataFrame) -> int:
"""Đếm số đánh giá mồ côi bằng phương thức isin.
Tham số:
rv: DataFrame chứa cột 'listing_id'.
ds: DataFrame chứa cột 'id'.
Trả về:
Số lượng dòng đánh giá không tìm thấy chỗ ở tương ứng (int).
"""
danh_sach_id = ds["id"]
mat_na_mo_coi = ~rv["listing_id"].isin(danh_sach_id)
return int(mat_na_mo_coi.sum())
# Dữ liệu thử nghiệm
ds_thieu = DEMO_DS[DEMO_DS["id"] != 11]
so_mo_coi_chuan = orphan_reviews_isin(DEMO_RV, DEMO_DS)
so_mo_coi_thieu = orphan_reviews_isin(DEMO_RV, ds_thieu)
print("Số đánh giá mồ côi trên dữ liệu chuẩn:", so_mo_coi_chuan)
print("Số đánh giá mồ côi khi khuyết chỗ ở 11:", so_mo_coi_thieu)
# Kết thúc lời giảiGiải thích cách 1
isin(ds["id"]) kiểm tra mã chỗ ở của từng dòng đánh giá có trong bảng danh mục hay không. Đảo mặt nạ bằng ~ rồi cộng boolean đếm số dòng không khớp. Một mã không khớp lặp ba lần vẫn tạo ba dòng được đếm.
Kết quả cách 1
| Kết quả |
|---|
| 7 |
Cách 2: Sử dụng phép ghép nối Anti-Join
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
ds_thieu = DEMO_DS[DEMO_DS["id"] != 11]
def orphan_reviews_isin(rv: pd.DataFrame, ds: pd.DataFrame) -> int:
"""Đếm số đánh giá mồ côi bằng phương thức isin.
Tham số:
rv: DataFrame chứa cột 'listing_id'.
ds: DataFrame chứa cột 'id'.
Trả về:
Số lượng dòng đánh giá không tìm thấy chỗ ở tương ứng (int).
"""
danh_sach_id = ds["id"]
mat_na_mo_coi = ~rv["listing_id"].isin(danh_sach_id)
return int(mat_na_mo_coi.sum())
so_mo_coi_chuan = orphan_reviews_isin(DEMO_RV, DEMO_DS)
so_mo_coi_thieu = orphan_reviews_isin(DEMO_RV, ds_thieu)
# Bắt đầu lời giải
def orphan_reviews(rv: pd.DataFrame, ds: pd.DataFrame) -> int:
"""Đếm số đánh giá mồ côi bằng kỹ thuật anti-join.
Tham số:
rv: DataFrame chứa cột 'listing_id'.
ds: DataFrame chứa cột 'id'.
Trả về:
Số lượng đánh giá mồ côi (int).
"""
ghep = rv[["listing_id"]].merge(
ds[["id"]], left_on="listing_id", right_on="id", how="left", indicator=True
)
return int((ghep["_merge"] == "left_only").sum())
so_c2_chuan = orphan_reviews(DEMO_RV, DEMO_DS)
so_c2_thieu = orphan_reviews(DEMO_RV, ds_thieu)
assert (
so_mo_coi_chuan == so_c2_chuan and so_mo_coi_thieu == so_c2_thieu
), "Hai cách phải cho kết quả đồng nhất"
print("Xác nhận Cách 2: Anti-join hoàn tất")
# Kết thúc lời giảiGiải thích cách 2
Phép ghép có chỉ báo hoặc đối chiếu tập khóa làm rõ phía nào thiếu bản ghi. Phải giữ đơn vị đếm là dòng đánh giá, không đổi thành số mã khác nhau nếu đề không yêu cầu. Các khóa thiếu cũng cần được hiểu theo quy tắc của bài trước khi gọi kết quả là toàn vẹn.
Kết quả cách 2
text
Xác nhận Cách 2: Anti-join hoàn tấtSo sánh cách dùng
Hãy chọn Cách 1 khi bạn chỉ cần đếm nhanh số lượng dòng đánh giá mang mã chỗ ở không tồn tại trong danh mục nguồn. Cách 2 hữu ích hơn khi cần giữ lại toàn bộ các cột thông tin của bản ghi mồ côi để truy vết nguyên nhân sai lệch khóa ngoại.
Bài 8: Đánh giá ứng viên khóa tự nhiên trên tập thuộc tính
Mục tiêu: Đánh giá tính khả thi của việc sử dụng tập thuộc tính sẵn có (listing_id, date) làm khóa tự nhiên trong bảng đánh giá thông qua hàm duplicate_pairs. Xác định số lượng bản ghi bị trùng lặp theo danh sách cột chỉ định mà không làm biến đổi bảng đầu vào.
Dữ liệu đầu vào: Bảng đánh giá mẫu DEMO_RV (18 dòng), danh sách cột kiểm tra mặc định cot = ["listing_id", "date"].
Thao tác yêu cầu:
- Viết hàm
duplicate_pairs(rv: pd.DataFrame, cot: list = None) -> intnhận bảng và danh sách cột ứng viên làm khóa, trả về số dòng lặp lại sau lần xuất hiện đầu tiên (duplicated(subset=cot)). - Kiểm tra trên danh sách mặc định
["listing_id", "date"]và danh sách đơn thuộc tính["listing_id"]. - Giải thích hệ quả kỹ thuật nếu người xử lý dữ liệu vội vàng gọi
drop_duplicates(subset=["listing_id", "date"]).
Trường hợp biên cần chú ý: Hàm chỉ đếm số lượng, tuyệt đối không chỉnh sửa dữ liệu, không xóa dòng của bảng đầu vào.
Lời giải
Cách 1: Đếm số lượng phần tử trùng lặp với duplicated
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
# Bắt đầu lời giải
def duplicate_pairs_duplicated(rv: pd.DataFrame, cot: list = None) -> int:
"""Đếm số dòng lặp lại theo danh sách cột bằng phương thức duplicated.
Tham số:
rv: DataFrame cần kiểm tra.
cot: Danh sách các cột ứng viên làm khóa (mặc định: listing_id và date).
Trả về:
Số lượng dòng lặp lại sau lần đầu tiên (int).
"""
if cot is None:
cot = ["listing_id", "date"]
return int(rv.duplicated(subset=cot).sum())
lap_c1_cap = duplicate_pairs_duplicated(DEMO_RV)
lap_c1_id = duplicate_pairs_duplicated(DEMO_RV, ["listing_id"])
print("Số dòng lặp theo cặp (listing_id, date):", lap_c1_cap)
print("Số dòng lặp theo riêng listing_id:", lap_c1_id)
# Kết thúc lời giảiGiải thích cách 1
duplicated với hai cột mã chỗ ở và ngày xác định các cặp giống nhau. Chọn keep=False khi đếm mọi dòng tham gia cặp lặp. Đó là phép kiểm khóa tự nhiên được đề xuất, chưa phải bằng chứng tất cả những đánh giá đó là bản sao.
Kết quả cách 1
| Kết quả |
|---|
| 13 |
Cách 2: So sánh độ dài trước và sau khi loại trừ giá trị trùng
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def duplicate_pairs_duplicated(rv: pd.DataFrame, cot: list = None) -> int:
"""Đếm số dòng lặp lại theo danh sách cột bằng phương thức duplicated.
Tham số:
rv: DataFrame cần kiểm tra.
cot: Danh sách các cột ứng viên làm khóa (mặc định: listing_id và date).
Trả về:
Số lượng dòng lặp lại sau lần đầu tiên (int).
"""
if cot is None:
cot = ["listing_id", "date"]
return int(rv.duplicated(subset=cot).sum())
lap_c1_cap = duplicate_pairs_duplicated(DEMO_RV)
lap_c1_id = duplicate_pairs_duplicated(DEMO_RV, ["listing_id"])
# Bắt đầu lời giải
def duplicate_pairs(rv: pd.DataFrame, cot: list = None) -> int:
"""Đếm số dòng lặp bằng cách đối chiếu độ dài tập duy nhất.
Tham số:
rv: DataFrame đánh giá.
cot: Danh sách cột khóa ứng viên.
Trả về:
Số lượng dòng lặp lại (int).
"""
if cot is None:
cot = ["listing_id", "date"]
so_dong_duy_nhat = len(rv.drop_duplicates(subset=cot))
return int(len(rv) - so_dong_duy_nhat)
lap_c2_cap = duplicate_pairs(DEMO_RV)
lap_c2_id = duplicate_pairs(DEMO_RV, ["listing_id"])
assert (
lap_c1_cap == lap_c2_cap and lap_c1_id == lap_c2_id
), "Hai cách phải cho kết quả đồng nhất"
print("Xác nhận Cách 2: Đối chiếu độ dài thành công")
# Kết thúc lời giảiGiải thích cách 2
Nhóm theo cặp khóa rồi đếm kích thước giúp xem phân bố các nhóm lặp. Tổng kích thước nhóm lớn hơn một khác với tổng số dòng dư sau khi giữ một đại diện. Giữ đúng định nghĩa cần trả và kiểm cả một cặp xuất hiện ba lần để thấy sự khác nhau.
Kết quả cách 2
text
Xác nhận Cách 2: Đối chiếu độ dài thành côngSo sánh cách dùng
Cách 1 kiểm từng dòng có tham gia một cặp khóa lặp hay không. Cách 2 đếm qua kích thước nhóm để đối chiếu cùng đại lượng. Tổng số dòng tham gia nhóm lặp khác số dòng dư sau khi giữ một đại diện của mỗi nhóm.
Bài 9: Đối chiếu cột dẫn xuất tổng số đánh giá với con số tự đếm
Mục tiêu: Kiểm tra tính nhất quán giữa cột dẫn xuất có sẵn number_of_reviews trong bảng listings với số dòng thực tế đếm được từ bảng reviews. Thiết lập bảng đối chiếu 3 cột nguon, dem_ca_file, den_moc cho toàn bộ các chỗ ở, phân tích ảnh hưởng của chính sách cắt mốc thời gian lên tính nhất quán của cột dẫn xuất.
Dữ liệu đầu vào: Bảng đánh giá mẫu DEMO_RV (18 dòng), bảng chỗ ở DEMO_DS (6 chỗ ở), mốc công bố MOC = "2026-06-29".
Thao tác yêu cầu:
- Viết hàm
review_count_check(rv: pd.DataFrame, ds: pd.DataFrame) -> dicttrả về từ điển đúng hai khóa:dem_that: Series kiểu số nguyên cùng vớids, thể hiện số dòng đánh giá thực tế của từng chỗ ở trên toàn bộ bảngrv, chỗ ở chưa có đánh giá nào phải nhận giá trị 0.so_lech_tong: Số lượng chỗ ở cónumber_of_reviewskhác vớidem_that.
- Viết hàm
doi_chieu_so_danh_gia(rv: pd.DataFrame, ds: pd.DataFrame, moc: str) -> pd.DataFrametrả về có làds["id"]và ba cột:nguon,dem_ca_file,den_moc. - Phân tích nguyên nhân vì sao
nguonkhớp 100% vớidem_ca_filenhưng lại có sự chênh lệch so vớiden_moc.
Trường hợp biên cần chú ý: Các chỗ ở có 0 đánh giá (như Studio nuevo mã 15) bắt buộc phải có giá trị đếm bằng 0, tuyệt đối không được để giá trị NaN trong kết quả đối chiếu.
Lời giải
Cách 1: Sử dụng groupby kết hợp reindex căn chỉnh chỉ mục
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
# Bắt đầu lời giải
def review_count_check_reindex(rv: pd.DataFrame, ds: pd.DataFrame) -> dict:
"""Đối chiếu tổng số đánh giá bằng groupby và reindex tuần tự.
Tham số:
rv: DataFrame chứa 'listing_id'.
ds: DataFrame chứa 'id' và 'number_of_reviews'.
Trả về:
Dict gồm 'dem_that' (Series) và 'so_lech_tong' (int).
"""
dem_nhom = rv.groupby("listing_id").size()
dem_theo_id = dem_nhom.reindex(ds["id"], fill_value=0)
dem_that = pd.Series(dem_theo_id.values, index=ds.index, dtype=int)
so_lech = int((ds["number_of_reviews"] != dem_that).sum())
return {"dem_that": dem_that, "so_lech_tong": so_lech}
def doi_chieu_so_danh_gia_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, moc: str
) -> pd.DataFrame:
"""Tạo bảng đối chiếu ba cột số lượng đánh giá theo từng phạm vi.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
moc: Mốc chụp công bố.
Trả về:
DataFrame có chỉ mục là ds['id'] và ba cột nguon, dem_ca_file, den_moc.
"""
moc_ts = pd.Timestamp(moc)
dem_ca_file = (
rv.groupby("listing_id").size().reindex(ds["id"], fill_value=0).astype(int)
)
dem_den_moc = (
rv[rv["date"] <= moc_ts]
.groupby("listing_id")
.size()
.reindex(ds["id"], fill_value=0)
.astype(int)
)
nguon = ds.set_index("id")["number_of_reviews"].astype(int)
return pd.DataFrame(
{
"nguon": nguon,
"dem_ca_file": dem_ca_file.values,
"den_moc": dem_den_moc.values,
},
index=ds["id"],
)
kq_q4_c1 = review_count_check_reindex(DEMO_RV, DEMO_DS)
bang_dc_c1 = doi_chieu_so_danh_gia_buoc(DEMO_RV, DEMO_DS, MOC)
print("Số chỗ ở lệch tổng số đánh giá:", kq_q4_c1["so_lech_tong"])
print(bang_dc_c1)
# Kết thúc lời giảiGiải thích cách 1
Đếm review theo mã chỗ ở rồi reindex theo thứ tự ID của bảng danh mục. Điền 0 cho ID không xuất hiện trong log vì đang đếm số sự kiện của bảng đã cung cấp. So sánh với cột tính sẵn cho biết hai nguồn đếm có khớp trên cùng phạm vi hay không.
Kết quả cách 1
| id | nguon | dem_ca_file | den_moc |
|---|---|---|---|
| 11 | 7 | 7 | 6 |
| 12 | 4 | 4 | 3 |
| 13 | 4 | 4 | 4 |
| 14 | 2 | 2 | 1 |
| 15 | 0 | 0 | 0 |
| 16 | 1 | 1 | 1 |
Cách 2: Sử dụng phương thức map và gán giá trị mặc định
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def review_count_check_reindex(rv: pd.DataFrame, ds: pd.DataFrame) -> dict:
"""Đối chiếu tổng số đánh giá bằng groupby và reindex tuần tự.
Tham số:
rv: DataFrame chứa 'listing_id'.
ds: DataFrame chứa 'id' và 'number_of_reviews'.
Trả về:
Dict gồm 'dem_that' (Series) và 'so_lech_tong' (int).
"""
dem_nhom = rv.groupby("listing_id").size()
dem_theo_id = dem_nhom.reindex(ds["id"], fill_value=0)
dem_that = pd.Series(dem_theo_id.values, index=ds.index, dtype=int)
so_lech = int((ds["number_of_reviews"] != dem_that).sum())
return {"dem_that": dem_that, "so_lech_tong": so_lech}
def doi_chieu_so_danh_gia_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, moc: str
) -> pd.DataFrame:
"""Tạo bảng đối chiếu ba cột số lượng đánh giá theo từng phạm vi.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
moc: Mốc chụp công bố.
Trả về:
DataFrame có chỉ mục là ds['id'] và ba cột nguon, dem_ca_file, den_moc.
"""
moc_ts = pd.Timestamp(moc)
dem_ca_file = (
rv.groupby("listing_id").size().reindex(ds["id"], fill_value=0).astype(int)
)
dem_den_moc = (
rv[rv["date"] <= moc_ts]
.groupby("listing_id")
.size()
.reindex(ds["id"], fill_value=0)
.astype(int)
)
nguon = ds.set_index("id")["number_of_reviews"].astype(int)
return pd.DataFrame(
{
"nguon": nguon,
"dem_ca_file": dem_ca_file.values,
"den_moc": dem_den_moc.values,
},
index=ds["id"],
)
kq_q4_c1 = review_count_check_reindex(DEMO_RV, DEMO_DS)
bang_dc_c1 = doi_chieu_so_danh_gia_buoc(DEMO_RV, DEMO_DS, MOC)
# Bắt đầu lời giải
def review_count_check(rv: pd.DataFrame, ds: pd.DataFrame) -> dict:
"""Hàm đối chiếu tổng số đánh giá chuẩn mực bằng map vector hóa.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
Trả về:
Dict gồm dem_that và so_lech_tong.
"""
dem = rv.groupby("listing_id").size()
dem_that = ds["id"].map(dem).fillna(0).astype(int)
so_lech = int((ds["number_of_reviews"] != dem_that).sum())
return {"dem_that": dem_that, "so_lech_tong": so_lech}
def doi_chieu_so_danh_gia(rv: pd.DataFrame, ds: pd.DataFrame, moc: str) -> pd.DataFrame:
"""Tạo bảng đối chiếu số lượng đánh giá vector hóa.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
moc: Mốc chụp công bố.
Trả về:
DataFrame gồm ba cột nguon, dem_ca_file, den_moc.
"""
dem_tat_ca = rv.groupby("listing_id").size()
dem_cat_moc = rv.loc[rv["date"] <= pd.Timestamp(moc)].groupby("listing_id").size()
return pd.DataFrame(
{
"nguon": ds.set_index("id")["number_of_reviews"],
"dem_ca_file": ds["id"].map(dem_tat_ca).fillna(0).astype(int).values,
"den_moc": ds["id"].map(dem_cat_moc).fillna(0).astype(int).values,
},
index=ds["id"],
)
kq_q4_c2 = review_count_check(DEMO_RV, DEMO_DS)
bang_dc_c2 = doi_chieu_so_danh_gia(DEMO_RV, DEMO_DS, MOC)
assert kq_q4_c1["dem_that"].equals(kq_q4_c2["dem_that"]) and bang_dc_c1.equals(
bang_dc_c2
)
print("Xác nhận Cách 2: Đối chiếu cột dẫn xuất thành công")
# Kết thúc lời giảiGiải thích cách 2
Ghép bảng đếm theo ID rồi căn lại giữ cho kết quả đi cùng từng dòng danh mục. Khóa bảng tra cần đủ tính duy nhất để phép ghép không nhân bản dòng. Dấu lệch chưa chứng minh một trong hai bảng sai, vì chúng có thể dùng phạm vi thời gian hoặc tiêu chí giữ review khác nhau.
Kết quả cách 2
text
Xác nhận Cách 2: Đối chiếu cột dẫn xuất thành côngSo sánh cách dùng
Hãy chọn Cách 1 khi bạn muốn tạo một chuỗi đếm độc lập được căn chỉnh chuẩn xác theo của bảng danh mục để làm phép trừ. Cách 2 thuận tiện hơn khi muốn gắn thêm cột đối chiếu trực tiếp vào khung dữ liệu hiện có để tiện quan sát.
Bài 10: Đối chiếu cột đánh giá 12 tháng gần nhất và phân tích định nghĩa cửa sổ
Mục tiêu: Kiểm tra tính nhất quán của cột dẫn xuất number_of_reviews_ltm (Last Twelve Months: 12 tháng gần nhất) bằng cách tự đếm số dòng trong cửa sổ thời gian. Khảo sát và so sánh hai quy ước cửa sổ: Cửa sổ mở không chặn trên (ket_thuc=None) theo mặc định của tài liệu gốc, và cửa sổ đóng có chặn trên tại mốc công bố (ket_thuc=moc).
Dữ liệu đầu vào: Bảng đánh giá mẫu DEMO_RV, bảng chỗ ở DEMO_DS, ngày bắt đầu cửa sổ LTM_BAT_DAU = "2025-06-30", mốc công bố MOC = "2026-06-29".
Thao tác yêu cầu:
- Viết hàm
ltm_mismatch(rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str | None = None) -> dicttrả về từ điển đúng ba khóa:ltm_that: Series kiểu số nguyên cùng vớids, thể hiện số đánh giá thỏa mãn điều kiện thời gian.do_lech: Series kiểu số nguyên cùng vớids, tính bằngnumber_of_reviews_ltm − ltm_that.so_lech_ltm: Số lượng chỗ ở códo_lechkhác 0.
- Viết hàm
doi_chieu_12_thang_co_dinh(rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str) -> tuple[pd.Series, pd.Series]thực hiện đối chiếu trên khoảng đóng 12 tháng cố định, trả về (dem_12_thang, lech_12_thang)vớilech_12_thang = dem_12_thang − number_of_reviews_ltm. - Phân tích phân bố của độ lệch và giải thích vì sao độ lệch tập trung ở giá trị
.
Trường hợp biên cần chú ý: Mặc định ket_thuc=None không được chặn trên (lấy toàn bộ các đánh giá từ bat_dau trở về sau). Khi có ket_thuc, khoảng thời gian bao gồm cả hai ngày biên.
Lời giải
Cách 1: Lọc dữ liệu điều kiện và tính toán tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
# Bắt đầu lời giải
def ltm_mismatch_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str | None = None
) -> dict:
"""Tính số đánh giá LTM và đo độ lệch theo cách tiếp cận tuần tự.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau: Chuỗi ngày bắt đầu (gồm cả ngày này).
ket_thuc: Chuỗi ngày kết thúc tùy chọn (None nếu không chặn trên).
Trả về:
Dict gồm ltm_that, do_lech, so_lech_ltm.
"""
mat_na = rv["date"] >= pd.Timestamp(bat_dau)
if ket_thuc is not None:
mat_na = mat_na & (rv["date"] <= pd.Timestamp(ket_thuc))
dem_nhom = rv[mat_na].groupby("listing_id").size()
ltm_that = pd.Series(
dem_nhom.reindex(ds["id"], fill_value=0).values, index=ds.index, dtype=int
)
do_lech = ds["number_of_reviews_ltm"] - ltm_that
so_lech_ltm = int((do_lech != 0).sum())
return {"ltm_that": ltm_that, "do_lech": do_lech, "so_lech_ltm": so_lech_ltm}
def doi_chieu_12_thang_co_dinh_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str
) -> tuple[pd.Series, pd.Series]:
"""Đối chiếu khoảng đóng 12 tháng cố định tuần tự.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau, ket_thuc: Hai đầu mút của khoảng ngày.
Trả về:
Tuple gồm (dem_12_thang, lech_12_thang).
"""
mat_na = (rv["date"] >= pd.Timestamp(bat_dau)) & (
rv["date"] <= pd.Timestamp(ket_thuc)
)
dem = (
rv[mat_na]
.groupby("listing_id")
.size()
.reindex(ds["id"], fill_value=0)
.astype(int)
)
dem.index = ds["id"]
nguon = ds.set_index("id")["number_of_reviews_ltm"].astype(int)
lech = dem - nguon
return dem, lech
kq_ltm_c1 = ltm_mismatch_buoc(DEMO_RV, DEMO_DS, LTM_BAT_DAU)
dem12_c1, lech12_c1 = doi_chieu_12_thang_co_dinh_buoc(
DEMO_RV, DEMO_DS, LTM_BAT_DAU, MOC
)
print("Kết quả LTM không chặn trên Cách 1:")
print("Số chỗ ở lệch LTM:", kq_ltm_c1["so_lech_ltm"])
print("Độ lệch từng chỗ ở:", kq_ltm_c1["do_lech"].tolist())
# Kết thúc lời giảiGiải thích cách 1
Lọc ngày theo cận dưới gồm chính ngày bắt đầu. Nếu có cận trên, áp dụng thêm điều kiện gồm ngày kết thúc. Đếm theo ID rồi căn về bảng danh mục, sau đó lấy số tính sẵn trừ số vừa đếm. Để ket_thuc=None có nghĩa không chặn trên, đúng như yêu cầu biến thể này.
Kết quả cách 1
| Vị trí | Kết quả |
|---|---|
| 0 | -1 |
| 1 | -1 |
| 2 | 0 |
| 3 | 0 |
| 4 | 0 |
| 5 | 0 |
Cách 2: Sử dụng phương thức between và vector hóa
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def ltm_mismatch_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str | None = None
) -> dict:
"""Tính số đánh giá LTM và đo độ lệch theo cách tiếp cận tuần tự.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau: Chuỗi ngày bắt đầu (gồm cả ngày này).
ket_thuc: Chuỗi ngày kết thúc tùy chọn (None nếu không chặn trên).
Trả về:
Dict gồm ltm_that, do_lech, so_lech_ltm.
"""
mat_na = rv["date"] >= pd.Timestamp(bat_dau)
if ket_thuc is not None:
mat_na = mat_na & (rv["date"] <= pd.Timestamp(ket_thuc))
dem_nhom = rv[mat_na].groupby("listing_id").size()
ltm_that = pd.Series(
dem_nhom.reindex(ds["id"], fill_value=0).values, index=ds.index, dtype=int
)
do_lech = ds["number_of_reviews_ltm"] - ltm_that
so_lech_ltm = int((do_lech != 0).sum())
return {"ltm_that": ltm_that, "do_lech": do_lech, "so_lech_ltm": so_lech_ltm}
def doi_chieu_12_thang_co_dinh_buoc(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str
) -> tuple[pd.Series, pd.Series]:
"""Đối chiếu khoảng đóng 12 tháng cố định tuần tự.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau, ket_thuc: Hai đầu mút của khoảng ngày.
Trả về:
Tuple gồm (dem_12_thang, lech_12_thang).
"""
mat_na = (rv["date"] >= pd.Timestamp(bat_dau)) & (
rv["date"] <= pd.Timestamp(ket_thuc)
)
dem = (
rv[mat_na]
.groupby("listing_id")
.size()
.reindex(ds["id"], fill_value=0)
.astype(int)
)
dem.index = ds["id"]
nguon = ds.set_index("id")["number_of_reviews_ltm"].astype(int)
lech = dem - nguon
return dem, lech
kq_ltm_c1 = ltm_mismatch_buoc(DEMO_RV, DEMO_DS, LTM_BAT_DAU)
dem12_c1, lech12_c1 = doi_chieu_12_thang_co_dinh_buoc(
DEMO_RV, DEMO_DS, LTM_BAT_DAU, MOC
)
# Bắt đầu lời giải
def ltm_mismatch(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str | None = None
) -> dict:
"""Hàm đối chiếu LTM chuẩn mực hỗ trợ cả hai quy ước cửa sổ.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau: Ngày bắt đầu.
ket_thuc: Ngày kết thúc tùy chọn.
Trả về:
Dict gồm ltm_that, do_lech, so_lech_ltm.
"""
ts_bat_dau = pd.Timestamp(bat_dau)
if ket_thuc is None:
loc_rv = rv.loc[rv["date"] >= ts_bat_dau]
else:
loc_rv = rv.loc[rv["date"].between(ts_bat_dau, pd.Timestamp(ket_thuc))]
dem = loc_rv.groupby("listing_id").size()
ltm_that = ds["id"].map(dem).fillna(0).astype(int)
ltm_that.index = ds.index
do_lech = ds["number_of_reviews_ltm"] - ltm_that
so_lech_ltm = int((do_lech != 0).sum())
return {"ltm_that": ltm_that, "do_lech": do_lech, "so_lech_ltm": so_lech_ltm}
def doi_chieu_12_thang_co_dinh(
rv: pd.DataFrame, ds: pd.DataFrame, bat_dau: str, ket_thuc: str
) -> tuple[pd.Series, pd.Series]:
"""Đối chiếu khoảng đóng 12 tháng vector hóa.
Tham số:
rv: DataFrame đánh giá.
ds: DataFrame chỗ ở.
bat_dau, ket_thuc: Chuỗi ngày biên.
Trả về:
Tuple gồm (dem_12_thang, lech_12_thang).
"""
loc_rv = rv.loc[rv["date"].between(pd.Timestamp(bat_dau), pd.Timestamp(ket_thuc))]
dem = loc_rv.groupby("listing_id").size()
dem_12 = ds["id"].map(dem).fillna(0).astype(int)
dem_12.index = ds["id"]
lech_12 = dem_12 - ds.set_index("id")["number_of_reviews_ltm"]
return dem_12, lech_12
kq_ltm_c2 = ltm_mismatch(DEMO_RV, DEMO_DS, LTM_BAT_DAU)
dem12_c2, lech12_c2 = doi_chieu_12_thang_co_dinh(DEMO_RV, DEMO_DS, LTM_BAT_DAU, MOC)
assert kq_ltm_c1["so_lech_ltm"] == kq_ltm_c2["so_lech_ltm"] and dem12_c1.equals(
dem12_c2
)
print("Xác nhận Cách 2: Đối chiếu LTM thành công")
# Kết thúc lời giảiGiải thích cách 2
Tách mặt nạ thời gian, bảng đếm và phép đối chiếu giúp kiểm lại từng giả thiết về 12 tháng. Xem cả phân bố độ lệch, không chỉ đếm số dòng khác 0. Nếu thay cận trên bằng snapshot, phải tính lại và ghi đó là một định nghĩa cửa sổ khác.
Kết quả cách 2
text
Xác nhận Cách 2: Đối chiếu LTM thành côngSo sánh cách dùng
Cách 1 giúp tái hiện phép đếm từ mốc bắt đầu mà chưa chặn phía sau snapshot. Cách 2 dùng một khoảng ngày cố định. Khi có review mang ngày sau snapshot, hai quy tắc có thể cho số đếm khác nhau và cần được ghi rõ trong báo cáo.
Bài 11: Đóng gói báo cáo kiểm tra chất lượng dữ liệu
Mục tiêu: Đóng gói toàn bộ các chỉ số kiểm định chất lượng dữ liệu chéo bảng thành một báo cáo chuẩn mực qa_report gồm đúng ba cột, ghi xuất ra tệp định dạng CSV và đọc lại để kiểm tra tính nguyên vẹn. Xây dựng hàm tom_tat_co tính toán tỷ lệ phần trăm vi phạm theo từng bảng và trích xuất danh sách các dòng bị gắn cờ kèm dữ liệu gốc.
Dữ liệu đầu vào:
- Từ điển số liệu kiểm tra
so_lieu: Chứa số dòng vi phạm của các quy tắc (review_sau_moc,review_mo_coi,trung_theo_cap,lech_tong_review,lech_ltm). - Từ điển ghi chú nghiệp vụ
ghi_chu: Chứa diễn giải tương ứng của từng quy tắc.
Thao tác yêu cầu:
- Viết hàm
qa_report(so_dong: dict, ghi_chu: dict, path) -> pd.DataFrametạo gồm ba cộtquy_tac,so_dong,ghi_chu. Bảo toàn thứ tự khóa củaso_dong, giữ nguyên các quy tắc có 0 dòng vi phạm, điền chuỗi rỗng""cho quy tắc không có ghi chú. Xuất tệp CSV vớiindex=Falsevà đọc lại bằngpd.read_csv(path, keep_default_na=False). - Viết hàm
tom_tat_co(co: pd.DataFrame, ten_bang: str) -> pd.DataFrametrả về bảng tổng kết gồm các cột:quy_tac,bang,so_hang,tong_hang,ty_le_pct. - Trích xuất các hàng có ít nhất một cờ vi phạm bằng
co.any(axis=1)và ghép nối với dữ liệu gốc bằngjoin.
Trường hợp biên cần chú ý: Khi ghi và đọc lại CSV, tham số keep_default_na=False giúp chuỗi ghi chú rỗng không bị pandas tự động chuyển thành giá trị NaN.
Lời giải
Cách 1: Xây dựng bảng từ danh sách bản ghi tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def kiem_reviews_buoc(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ kiểm tra đánh giá theo từng bước.
Tham số:
r: DataFrame chứa 'listing_id' và 'date'.
ids: Tập hợp hoặc Series các mã chỗ ở hợp lệ.
moc: Mốc chụp công bố.
Trả về:
DataFrame gồm ba cột cờ boolean: ngay_thieu, sau_moc, id_khong_khop.
"""
moc_ts = pd.Timestamp(moc)
co_ngay_thieu = r["date"].isna()
co_sau_moc = r["date"].notna() & (r["date"] > moc_ts)
co_id_khong_khop = ~r["listing_id"].isin(ids)
return pd.DataFrame(
{
"ngay_thieu": co_ngay_thieu.astype(bool),
"sau_moc": co_sau_moc.astype(bool),
"id_khong_khop": co_id_khong_khop.astype(bool),
},
index=r.index,
)
def kiem_reviews(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ boolean kiểm tra đánh giá vector hóa.
Tham số:
r: DataFrame có 'listing_id' và 'date'.
ids: Danh sách mã chỗ ở hợp lệ.
moc: Mốc công bố.
Trả về:
DataFrame gồm ba cột cờ boolean.
"""
d = r["date"]
moc_ts = pd.Timestamp(moc)
return pd.DataFrame(
{
"ngay_thieu": d.isna(),
"sau_moc": (d.notna() & (d > moc_ts)).fillna(False).astype(bool),
"id_khong_khop": (~r["listing_id"].isin(ids)).astype(bool),
},
index=r.index,
)
co_rv_c1 = kiem_reviews_buoc(DEMO_RV, DEMO_DS["id"], MOC)
co_rv_c2 = kiem_reviews(DEMO_RV, DEMO_DS["id"], MOC)
# Bắt đầu lời giải
import tempfile
from pathlib import Path
def qa_report_records(so_dong: dict, ghi_chu: dict, path) -> pd.DataFrame:
"""Tạo và ghi báo cáo QA từ danh sách bản ghi tuần tự.
Tham số:
so_dong: Dict {ten_quy_tac: so_dong_vi_pham}.
ghi_chu: Dict {ten_quy_tac: chuoi_ghi_chu}.
path: Đường dẫn lưu tệp CSV.
Trả về:
DataFrame đọc lại từ tệp CSV vừa ghi.
"""
ban_ghi = []
for quy_tac, so in so_dong.items():
note = ghi_chu.get(quy_tac, "")
ban_ghi.append({"quy_tac": quy_tac, "so_dong": int(so), "ghi_chu": str(note)})
df = pd.DataFrame(ban_ghi)
df.to_csv(path, index=False, encoding="utf-8")
return pd.read_csv(path, keep_default_na=False)
def tom_tat_co_buoc(co: pd.DataFrame, ten_bang: str) -> pd.DataFrame:
"""Tạo bảng tóm tắt tỷ lệ cờ vi phạm tuần tự.
Tham số:
co: DataFrame các cột cờ boolean.
ten_bang: Tên bảng dữ liệu ('listings' hoặc 'reviews').
Trả về:
DataFrame tóm tắt tỷ lệ.
"""
tong = len(co)
hang = []
for col in co.columns:
so_vi_pham = int(co[col].sum())
ty_le = round((so_vi_pham / tong) * 100, 2) if tong > 0 else 0.0
hang.append(
{
"quy_tac": col,
"bang": ten_bang,
"so_hang": so_vi_pham,
"tong_hang": tong,
"ty_le_pct": ty_le,
}
)
return pd.DataFrame(hang)
# Dữ liệu mẫu thử nghiệm
so_lieu_demo = {
"review_sau_moc": 3,
"review_mo_coi": 0,
"trung_theo_cap": 3,
"lech_tong_review": 0,
"lech_ltm": 2,
}
ghi_chu_demo = {
"review_sau_moc": "date > mốc công bố 2026-06-29",
"review_mo_coi": "listing_id không có trong listings",
"trung_theo_cap": "không xóa do không phải khóa tự nhiên",
"lech_tong_review": "cột dẫn xuất so với số tự đếm",
"lech_ltm": "khác định nghĩa mốc 12 tháng",
}
with tempfile.TemporaryDirectory() as thu_muc_tam:
duong_dan_tam = Path(thu_muc_tam) / "qa_report_c1.csv"
bao_cao_c1 = qa_report_records(so_lieu_demo, ghi_chu_demo, duong_dan_tam)
bang_tt_c1 = tom_tat_co_buoc(co_rv_c1, "reviews")
print("Nội dung báo cáo QA Cách 1:")
print(bao_cao_c1)
print("Bảng tóm tắt cờ vi phạm Cách 1:")
print(bang_tt_c1)
# Kết thúc lời giảiGiải thích cách 1
Dựng từng bản ghi báo cáo theo thứ tự khóa của dict số dòng. Quy tắc có số dòng bằng 0 vẫn phải xuất hiện để phân biệt đã kiểm và không kiểm. to_csv(index=False) bỏ phụ, rồi đọc lại tệp để xác nhận cấu trúc xuất ra.
Kết quả cách 1
| quy_tac | bang | so_hang | tong_hang | ty_le_pct | |
|---|---|---|---|---|---|
| 0 | ngay_thieu | reviews | 0 | 18 | 0 |
| 1 | sau_moc | reviews | 3 | 18 | 16.67 |
| 2 | id_khong_khop | reviews | 0 | 18 | 0 |
Cách 2: Sử dụng phương thức khởi tạo DataFrame từ từ điển và chuỗi thao tác
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
MOC = "2026-06-29"
LTM_BAT_DAU = "2025-06-30"
DEMO_RV = pd.DataFrame(
{
"listing_id": [
11,
11,
11,
11,
11,
11,
11,
12,
12,
12,
12,
13,
13,
13,
13,
14,
14,
16,
],
"date": pd.to_datetime(
[
"2019-03-01",
"2024-12-01",
"2025-06-30",
"2025-08-10",
"2025-08-10",
"2026-06-29",
"2026-06-30",
"2010-11-13",
"2025-06-30",
"2026-01-05",
"2026-07-01",
"2023-04-04",
"2023-04-04",
"2023-04-04",
"2025-07-01",
"2025-12-24",
"2026-06-30",
"2021-02-02",
]
),
}
)
DEMO_DS = pd.DataFrame(
{
"id": [11, 12, 13, 14, 15, 16],
"name": [
"Depto Lastarria",
"Pieza Bellavista",
"Loft Providencia",
"Casa Ñuñoa",
"Studio nuevo",
"Hab Centro",
],
"number_of_reviews": [7, 4, 4, 2, 0, 1],
"number_of_reviews_ltm": [4, 2, 1, 2, 0, 0],
"last_review": pd.to_datetime(
["2026-06-30", "2026-07-01", "2025-07-01", "2026-06-30", None, "2021-02-02"]
),
}
)
def kiem_reviews_buoc(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ kiểm tra đánh giá theo từng bước.
Tham số:
r: DataFrame chứa 'listing_id' và 'date'.
ids: Tập hợp hoặc Series các mã chỗ ở hợp lệ.
moc: Mốc chụp công bố.
Trả về:
DataFrame gồm ba cột cờ boolean: ngay_thieu, sau_moc, id_khong_khop.
"""
moc_ts = pd.Timestamp(moc)
co_ngay_thieu = r["date"].isna()
co_sau_moc = r["date"].notna() & (r["date"] > moc_ts)
co_id_khong_khop = ~r["listing_id"].isin(ids)
return pd.DataFrame(
{
"ngay_thieu": co_ngay_thieu.astype(bool),
"sau_moc": co_sau_moc.astype(bool),
"id_khong_khop": co_id_khong_khop.astype(bool),
},
index=r.index,
)
def kiem_reviews(r: pd.DataFrame, ids, moc: str) -> pd.DataFrame:
"""Tạo bảng cờ boolean kiểm tra đánh giá vector hóa.
Tham số:
r: DataFrame có 'listing_id' và 'date'.
ids: Danh sách mã chỗ ở hợp lệ.
moc: Mốc công bố.
Trả về:
DataFrame gồm ba cột cờ boolean.
"""
d = r["date"]
moc_ts = pd.Timestamp(moc)
return pd.DataFrame(
{
"ngay_thieu": d.isna(),
"sau_moc": (d.notna() & (d > moc_ts)).fillna(False).astype(bool),
"id_khong_khop": (~r["listing_id"].isin(ids)).astype(bool),
},
index=r.index,
)
co_rv_c1 = kiem_reviews_buoc(DEMO_RV, DEMO_DS["id"], MOC)
co_rv_c2 = kiem_reviews(DEMO_RV, DEMO_DS["id"], MOC)
so_lieu_demo = {
"review_sau_moc": 3,
"review_mo_coi": 0,
"trung_theo_cap": 3,
"lech_tong_review": 0,
"lech_ltm": 2,
}
ghi_chu_demo = {
"review_sau_moc": "date > mốc công bố 2026-06-29",
"review_mo_coi": "listing_id không có trong listings",
"trung_theo_cap": "không xóa do không phải khóa tự nhiên",
"lech_tong_review": "cột dẫn xuất so với số tự đếm",
"lech_ltm": "khác định nghĩa mốc 12 tháng",
}
def qa_report_records(so_dong: dict, ghi_chu: dict, path) -> pd.DataFrame:
"""Tạo và ghi báo cáo QA từ danh sách bản ghi tuần tự.
Tham số:
so_dong: Dict {ten_quy_tac: so_dong_vi_pham}.
ghi_chu: Dict {ten_quy_tac: chuoi_ghi_chu}.
path: Đường dẫn lưu tệp CSV.
Trả về:
DataFrame đọc lại từ tệp CSV vừa ghi.
"""
ban_ghi = []
for quy_tac, so in so_dong.items():
note = ghi_chu.get(quy_tac, "")
ban_ghi.append({"quy_tac": quy_tac, "so_dong": int(so), "ghi_chu": str(note)})
df = pd.DataFrame(ban_ghi)
df.to_csv(path, index=False, encoding="utf-8")
return pd.read_csv(path, keep_default_na=False)
def tom_tat_co_buoc(co: pd.DataFrame, ten_bang: str) -> pd.DataFrame:
"""Tạo bảng tóm tắt tỷ lệ cờ vi phạm tuần tự.
Tham số:
co: DataFrame các cột cờ boolean.
ten_bang: Tên bảng dữ liệu ('listings' hoặc 'reviews').
Trả về:
DataFrame tóm tắt tỷ lệ.
"""
tong = len(co)
hang = []
for col in co.columns:
so_vi_pham = int(co[col].sum())
ty_le = round((so_vi_pham / tong) * 100, 2) if tong > 0 else 0.0
hang.append(
{
"quy_tac": col,
"bang": ten_bang,
"so_hang": so_vi_pham,
"tong_hang": tong,
"ty_le_pct": ty_le,
}
)
return pd.DataFrame(hang)
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as report_folder:
bao_cao_c1 = qa_report_records(
so_lieu_demo, ghi_chu_demo, Path(report_folder) / "reference.csv"
)
bang_tt_c1 = tom_tat_co_buoc(co_rv_c1, "reviews")
# Bắt đầu lời giải
def qa_report(so_dong: dict, ghi_chu: dict, path) -> pd.DataFrame:
"""Hàm tạo báo cáo QA chuẩn mực của hệ thống.
Tham số:
so_dong: Dict số dòng vi phạm.
ghi_chu: Dict ghi chú nghiệp vụ.
path: Đường dẫn tệp đích.
Trả về:
DataFrame đọc lại từ file.
"""
danh_sach_qt = list(so_dong.keys())
df = pd.DataFrame(
{
"quy_tac": danh_sach_qt,
"so_dong": [int(so_dong[k]) for k in danh_sach_qt],
"ghi_chu": [str(ghi_chu.get(k, "")) for k in danh_sach_qt],
}
)
df.to_csv(path, index=False, encoding="utf-8")
return pd.read_csv(path, keep_default_na=False)
def tom_tat_co(co: pd.DataFrame, ten_bang: str) -> pd.DataFrame:
"""Tạo bảng tóm tắt tỷ lệ cờ vi phạm vector hóa.
Tham số:
co: DataFrame boolean các cờ.
ten_bang: Tên bảng.
Trả về:
DataFrame tóm tắt gồm quy_tac, bang, so_hang, tong_hang, ty_le_pct.
"""
bao_cao = pd.DataFrame(
{
"bang": ten_bang,
"so_hang": co.sum().astype(int),
"tong_hang": len(co),
"ty_le_pct": (co.mean() * 100).round(2),
}
)
return bao_cao.rename_axis("quy_tac").reset_index()
with tempfile.TemporaryDirectory() as thu_muc_tam:
duong_dan_tam = Path(thu_muc_tam) / "qa_report_c2.csv"
bao_cao_c2 = qa_report(so_lieu_demo, ghi_chu_demo, duong_dan_tam)
bang_tt_c2 = tom_tat_co(co_rv_c2, "reviews")
assert bao_cao_c1.equals(bao_cao_c2) and bang_tt_c1.equals(
bang_tt_c2
), "Kết quả hai cách phải đồng nhất tuyệt đối"
print("Xác nhận Cách 2: Xuất báo cáo và tóm tắt cờ thành công")
# Kết thúc lời giảiGiải thích cách 2
Tạo trực tiếp từ dữ liệu có cấu trúc giảm bước ghép từng dòng. ghi_chu.get(..., "") cho ghi chú rỗng khi chưa có nội dung, không làm mất quy tắc. keep_default_na=False khi đọc lại giữ chuỗi rỗng thay vì đổi thành .
Kết quả cách 2
text
Xác nhận Cách 2: Xuất báo cáo và tóm tắt cờ thành côngSo sánh cách dùng
Hãy chọn Cách 1 khi các chỉ số kiểm tra được thu thập rời rạc và bạn muốn chủ động ghép từng dòng theo thứ tự sẵn có. Cách 2 thích hợp khi dữ liệu đã có sẵn dạng từ điển và bạn muốn tạo ngay bảng tổng kết để xuất tệp CSV.
Bài 12: Tách nhóm khóa có giá trị mâu thuẫn trước khi hợp nhất
Mục tiêu: Xử lý dữ liệu bảng giá nhập gia_nhap thu thập từ nhiều đợt xuất khác nhau theo khóa tổ hợp (id, ky). Phân tách dữ liệu thành hai bảng riêng biệt: Bảng gia_gop gồm đúng một hàng cho mỗi khóa không có mâu thuẫn, và bảng gia_can_xem lưu giữ toàn bộ các hàng của những khóa có từ hai giá trị đã biết khác nhau.
Dữ liệu đầu vào: Bảng gia_nhap gồm 9 hàng, có ba cột id, ky, gia (USD/đêm) với các kịch bản: Hai giá giống nhau, một giá đã biết kèm một ô thiếu, toàn bộ ô thiếu, và hai giá đã biết khác nhau.
Thao tác yêu cầu:
- Xác định điều kiện mâu thuẫn: Một khóa
(id, ky)bị coi là mâu thuẫn khi và chỉ khi nó có từ 2 giá trị đã biết khác nhau trở lên (s.dropna().nunique() > 1). - Xây dựng bảng
gia_gop: Chứa các khóa không mâu thuẫn (mỗi khóa đúng một hàng). Nếu khóa có một giá đã biết và ô thiếu, giữ giá đã biết, còn nếu nhóm toàn ô thiếu, giữ nguyênNaN. - Xây dựng bảng
gia_can_xem: Giữ nguyên toàn bộ các dòng của các khóa có mâu thuẫn giá để phục vụ thẩm định chuyên sâu. - Bảo đảm không làm thay đổi bảng dữ liệu ban đầu
gia_nhap.
Trường hợp biên cần chú ý: Trường hợp một khóa có một giá 110 và một giá trị NaN không cấu thành mâu thuẫn, chỉ phản ánh một đợt xuất thiếu thông tin.
Lời giải
Cách 1: Phân tách nhóm bằng vòng lặp và tập hợp giá trị duy nhất
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def tach_gia_mau_thuan_vong_lap(
gia_nhap: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Tách nhóm giá mâu thuẫn bằng vòng lặp nhóm tuần tự.
Tham số:
gia_nhap: DataFrame chứa 'id', 'ky', 'gia'.
Trả về:
Tuple gồm (gia_gop, gia_can_xem).
"""
df = gia_nhap.copy()
nhom = df.groupby(["id", "ky"])
hang_can_xem = []
hang_gop = []
for (ma_id, ma_ky), grp in nhom:
gia_khong_na = grp["gia"].dropna().unique()
if len(gia_khong_na) > 1:
hang_can_xem.append(grp)
else:
val = gia_khong_na[0] if len(gia_khong_na) == 1 else np.nan
hang_gop.append({"id": ma_id, "ky": ma_ky, "gia": val})
gia_can_xem = (
pd.concat(hang_can_xem).sort_index()
if hang_can_xem
else pd.DataFrame(columns=["id", "ky", "gia"])
)
gia_gop = pd.DataFrame(hang_gop)
return gia_gop, gia_can_xem
# Dữ liệu thử nghiệm minh họa
gia_nhap_demo = pd.DataFrame(
{
"id": ["P1", "P2", "P3", "P4", "P1", "P2", "P3", "P4", "P1"],
"ky": ["T6", "T6", "T6", "T6", "T6", "T6", "T6", "T6", "T5"],
"gia": [120.0, 90.0, np.nan, np.nan, 120.0, 95.0, 110.0, np.nan, 100.0],
}
)
gop_c1, xem_c1 = tach_gia_mau_thuan_vong_lap(gia_nhap_demo)
print("Bảng hợp nhất gia_gop Cách 1:")
print(gop_c1)
print("Các hàng mâu thuẫn gia_can_xem Cách 1:")
print(xem_c1)
# Kết thúc lời giảiGiải thích cách 1
Nhóm các bản ghi có cùng khóa rồi xem số mức giá phân biệt. Những nhóm có nhiều giá phải được giữ riêng để đối chiếu, không chọn tùy ý dòng đầu rồi gọi là làm sạch. Các dòng không mâu thuẫn có thể chuyển sang bước gộp theo đúng quy tắc đã nêu.
Kết quả cách 1
| id | ky | gia | |
|---|---|---|---|
| 1 | P2 | T6 | 90 |
| 5 | P2 | T6 | 95 |
Cách 2: Sử dụng transform vector hóa và lọc chỉ mục
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
gia_nhap_demo = pd.DataFrame(
{
"id": ["P1", "P2", "P3", "P4", "P1", "P2", "P3", "P4", "P1"],
"ky": ["T6", "T6", "T6", "T6", "T6", "T6", "T6", "T6", "T5"],
"gia": [120.0, 90.0, np.nan, np.nan, 120.0, 95.0, 110.0, np.nan, 100.0],
}
)
def tach_gia_mau_thuan_vong_lap(
gia_nhap: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Tách nhóm giá mâu thuẫn bằng vòng lặp nhóm tuần tự.
Tham số:
gia_nhap: DataFrame chứa 'id', 'ky', 'gia'.
Trả về:
Tuple gồm (gia_gop, gia_can_xem).
"""
df = gia_nhap.copy()
nhom = df.groupby(["id", "ky"])
hang_can_xem = []
hang_gop = []
for (ma_id, ma_ky), grp in nhom:
gia_khong_na = grp["gia"].dropna().unique()
if len(gia_khong_na) > 1:
hang_can_xem.append(grp)
else:
val = gia_khong_na[0] if len(gia_khong_na) == 1 else np.nan
hang_gop.append({"id": ma_id, "ky": ma_ky, "gia": val})
gia_can_xem = (
pd.concat(hang_can_xem).sort_index()
if hang_can_xem
else pd.DataFrame(columns=["id", "ky", "gia"])
)
gia_gop = pd.DataFrame(hang_gop)
return gia_gop, gia_can_xem
gop_c1, xem_c1 = tach_gia_mau_thuan_vong_lap(gia_nhap_demo)
# Bắt đầu lời giải
def tach_gia_mau_thuan(gia_nhap: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Tách nhóm giá mâu thuẫn bằng kỹ thuật transform vector hóa.
Tham số:
gia_nhap: DataFrame có 'id', 'ky', 'gia'.
Trả về:
Tuple gồm (gia_gop, gia_can_xem).
"""
df = gia_nhap.copy()
so_gia_rieng = df.groupby(["id", "ky"])["gia"].transform(
lambda s: s.dropna().nunique()
)
gia_can_xem = df[so_gia_rieng > 1].copy()
khong_mau_thuan = df[so_gia_rieng <= 1]
gia_gop = (
khong_mau_thuan.sort_values(by=["id", "ky", "gia"], na_position="last")
.drop_duplicates(subset=["id", "ky"], keep="first")
.reset_index(drop=True)
)
return gia_gop, gia_can_xem
gop_c2, xem_c2 = tach_gia_mau_thuan(gia_nhap_demo)
assert len(gop_c1) == len(gop_c2) and len(xem_c1) == len(
xem_c2
), "Hai cách phải cho kết quả đồng nhất"
print("Xác nhận Cách 2: Tách nhóm mâu thuẫn vector hóa hoàn tất")
# Kết thúc lời giảiGiải thích cách 2
nunique và transform tạo cờ mâu thuẫn tương ứng từng dòng. Phép chọn bằng cờ giữ cả nhóm để kiểm tra nguồn, ngày hoặc đơn vị. Cần thống nhất cách xử lý giá thiếu khi đếm mức giá, vì một ô thiếu không tự mang một mức giá mới.
Kết quả cách 2
text
Xác nhận Cách 2: Tách nhóm mâu thuẫn vector hóa hoàn tấtSo sánh cách dùng
Cách 1 mở từng nhóm chỗ ở và ngày để xem các mức giá mâu thuẫn. Cách 2 gắn cùng quy tắc kiểm lên bảng để lọc các dòng liên quan. Hãy giữ cả giá trị gốc và cờ mâu thuẫn trước khi quyết định cách gộp.
Bài 13: Điền mốt, truyền giá trị phía trước theo nhóm và nội suy thời gian
Mục tiêu: Làm chủ ba phương pháp điền khuyết nâng cao cho từng loại cấu trúc dữ liệu đặc thù:
- Điền mốt (mode - giá trị xuất hiện nhiều nhất) cho biến phân loại danh mục
loai. - Sắp xếp dữ liệu theo khóa đa cấp
(id, ky)và truyền giá trị đã biết phía trước (ffill) riêng biệt theo từng chỗ ở với giới hạnlimit=1trên bảnglich_gia. - Nội suy theo khoảng cách thời gian (
method="time") trên chuỗi thời giannhiet_docó ngày không cách đều, bảo đảm không ngoại suy ngoài miền quan sát.
Dữ liệu đầu vào:
- Series
loai_demogồm 5 chỗ ở, chỗ ở P4 bị thiếu loại phòng. - Bảng
lich_demogồm 8 hàng chưa được sắp xếp, ghi nhận giá qua 4 kỳ của hai chỗ ở P1 và P2. - Series
nhiet_do_demoghi nhận nhiệt độ qua các ngày không liên tục (ngày 01, 02, 05, 07, 08 tháng 06 năm 2026).
Thao tác yêu cầu:
- Tạo Series
loai_dien: Điền ô thiếu bằng giá trị mốt của các loại phòng đã biết. - Tạo
lich_dien: Sắp xếp theoidrồiky, thêm cộtdienbằng giá trị phía trước của cùng chỗ ở, tối đa một ô thiếu liên tiếp (limit=1). - Tạo Series
noi_suy: Nội suy giá trị theo khoảng cách thời gian thực tế giữa các mốc ngày.
Trường hợp biên cần chú ý:
- Nếu không nhóm theo
idtrước khiffill, giá của chỗ ở này sẽ bị truyền nhầm sang chỗ ở khác. - Tại ngày cuối cùng của chuỗi thời gian (ngày 08/06), vì không có mốc dữ liệu phía sau để nội suy nên bắt buộc phải giữ nguyên
NaN.
Lời giải
Cách 1: Xử lý tuần tự từng bước logic
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
# Bắt đầu lời giải
def dien_khuyet_chuyen_sau_buoc(
loai: pd.Series, lich_gia: pd.DataFrame, nhiet_do: pd.Series
) -> tuple[pd.Series, pd.DataFrame, pd.Series]:
"""Thực hiện ba kỹ thuật điền khuyết chuyên sâu theo cách tuần tự.
Tham số:
loai: Series biến phân loại.
lich_gia: DataFrame chưa sắp xếp có 'id', 'ky', 'gia'.
nhiet_do: Series có DatetimeIndex không cách đều.
Trả về:
Tuple gồm (loai_dien, lich_dien, noi_suy).
"""
# 1. Điền mốt
gia_tri_mot = loai.mode()[0]
loai_dien = loai.fillna(gia_tri_mot)
# 2. Sắp xếp và ffill theo nhóm
lich_dien = lich_gia.sort_values(by=["id", "ky"]).reset_index(drop=True)
cot_dien = []
for _, grp in lich_dien.groupby("id", sort=False):
cot_dien.append(grp["gia"].ffill(limit=1))
lich_dien["dien"] = pd.concat(cot_dien).values
# 3. Nội suy theo thời gian
noi_suy = nhiet_do.interpolate(method="time", limit_area="inside")
return loai_dien, lich_dien, noi_suy
# Dữ liệu thử nghiệm minh họa
loai_demo = pd.Series(
["Nguyên căn", "Phòng riêng", "Nguyên căn", None, "Nguyên căn"],
index=["P1", "P2", "P3", "P4", "P5"],
)
lich_demo = pd.DataFrame(
{
"id": ["P2", "P1", "P1", "P2", "P1", "P2", "P1", "P2"],
"ky": [3, 1, 4, 1, 2, 2, 3, 4],
"gia": [np.nan, 100.0, 130.0, np.nan, np.nan, 200.0, np.nan, np.nan],
}
)
nhiet_do_demo = pd.Series(
[18.0, np.nan, np.nan, 24.0, np.nan],
index=pd.to_datetime(
["2026-06-01", "2026-06-02", "2026-06-05", "2026-06-07", "2026-06-08"]
),
)
loai_c1, lich_c1, ns_c1 = dien_khuyet_chuyen_sau_buoc(
loai_demo, lich_demo, nhiet_do_demo
)
print("Kết quả điền mốt Cách 1:", loai_c1)
print("Kết quả ffill theo nhóm Cách 1:", lich_c1)
print("Kết quả nội suy thời gian Cách 1:", ns_c1)
# Kết thúc lời giảiGiải thích cách 1
Mốt, giá trước đó và nội suy dùng những giả thiết khác nhau. Mốt dựa vào giá trị xuất hiện nhiều trong một nhóm, forward fill dùng quan sát trước, còn nội suy dùng vị trí giữa các mốc đã có. Kiểm thứ tự thời gian và ranh giới từng nhóm trước khi điền.
Kết quả cách 1
| Giá trị | |
|---|---|
| 2026-06-01 00:00:00 | 18 |
| 2026-06-02 00:00:00 | 19 |
| 2026-06-05 00:00:00 | 22 |
| 2026-06-07 00:00:00 | 24 |
| 2026-06-08 00:00:00 |
Cách 2: Sử dụng các phương thức chuyên dụng của pandas
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
loai_demo = pd.Series(
["Nguyên căn", "Phòng riêng", "Nguyên căn", None, "Nguyên căn"],
index=["P1", "P2", "P3", "P4", "P5"],
)
lich_demo = pd.DataFrame(
{
"id": ["P2", "P1", "P1", "P2", "P1", "P2", "P1", "P2"],
"ky": [3, 1, 4, 1, 2, 2, 3, 4],
"gia": [np.nan, 100.0, 130.0, np.nan, np.nan, 200.0, np.nan, np.nan],
}
)
nhiet_do_demo = pd.Series(
[18.0, np.nan, np.nan, 24.0, np.nan],
index=pd.to_datetime(
["2026-06-01", "2026-06-02", "2026-06-05", "2026-06-07", "2026-06-08"]
),
)
def dien_khuyet_chuyen_sau_buoc(
loai: pd.Series, lich_gia: pd.DataFrame, nhiet_do: pd.Series
) -> tuple[pd.Series, pd.DataFrame, pd.Series]:
"""Thực hiện ba kỹ thuật điền khuyết chuyên sâu theo cách tuần tự.
Tham số:
loai: Series biến phân loại.
lich_gia: DataFrame chưa sắp xếp có 'id', 'ky', 'gia'.
nhiet_do: Series có DatetimeIndex không cách đều.
Trả về:
Tuple gồm (loai_dien, lich_dien, noi_suy).
"""
# 1. Điền mốt
gia_tri_mot = loai.mode()[0]
loai_dien = loai.fillna(gia_tri_mot)
# 2. Sắp xếp và ffill theo nhóm
lich_dien = lich_gia.sort_values(by=["id", "ky"]).reset_index(drop=True)
cot_dien = []
for _, grp in lich_dien.groupby("id", sort=False):
cot_dien.append(grp["gia"].ffill(limit=1))
lich_dien["dien"] = pd.concat(cot_dien).values
# 3. Nội suy theo thời gian
noi_suy = nhiet_do.interpolate(method="time", limit_area="inside")
return loai_dien, lich_dien, noi_suy
loai_c1, lich_c1, ns_c1 = dien_khuyet_chuyen_sau_buoc(
loai_demo, lich_demo, nhiet_do_demo
)
# Bắt đầu lời giải
def dien_khuyet_chuyen_sau(
loai: pd.Series, lich_gia: pd.DataFrame, nhiet_do: pd.Series
) -> tuple[pd.Series, pd.DataFrame, pd.Series]:
"""Thực hiện điền mốt, ffill theo nhóm và nội suy thời gian vector hóa.
Tham số:
loai: Series phân loại.
lich_gia: DataFrame lịch giá.
nhiet_do: Series thời gian.
Trả về:
Tuple gồm (loai_dien, lich_dien, noi_suy).
"""
loai_dien = loai.fillna(loai.mode().iloc[0])
lich_dien = lich_gia.sort_values(by=["id", "ky"]).reset_index(drop=True)
lich_dien["dien"] = lich_dien.groupby("id")["gia"].ffill(limit=1)
noi_suy = nhiet_do.interpolate(method="time", limit_area="inside")
return loai_dien, lich_dien, noi_suy
loai_c2, lich_c2, ns_c2 = dien_khuyet_chuyen_sau(loai_demo, lich_demo, nhiet_do_demo)
assert loai_c1.equals(loai_c2) and lich_c1.equals(lich_c2) and ns_c1.equals(ns_c2)
print("Xác nhận Cách 2: Toàn bộ ba phương pháp điền khuyết hoàn tất")
# Kết thúc lời giảiGiải thích cách 2
Các phương thức theo nhóm và giúp áp dụng quy tắc nhất quán. Không để forward fill chảy từ chỗ ở này sang chỗ ở khác hoặc dùng dữ liệu tương lai trong bài toán dự báo. Nội suy ở giữa hai mốc có căn cứ khác với ngoại suy ra đầu hoặc cuối chuỗi.
Kết quả cách 2
text
Xác nhận Cách 2: Toàn bộ ba phương pháp điền khuyết hoàn tấtSo sánh cách dùng
Cách 1 mở riêng từng thao tác điền mốt, lan truyền giá và nội suy. Cách 2 thể hiện các phép xử lý theo nhóm và . Cả hai vẫn cần giả thiết phù hợp, thứ tự thời gian đúng và giới hạn nhóm rõ ràng trước khi điền.
Tổng kết bài học và hướng dẫn thực hành chuyên sâu
Quy trình kiểm định chất lượng và làm sạch dữ liệu chéo bảng đòi hỏi sự kết hợp chặt chẽ giữa tư duy thống kê toán học và sự thấu hiểu sâu sắc bản chất nghiệp vụ dữ liệu:
- Điền khuyết có căn cứ: Chỉ điền 0 cho biến đếm khi hiện tượng chắc chắn chưa từng phát sinh, còn đối với biến đo lường chất lượng hoặc giá cả, việc điền trung vị theo nhóm phân loại phải luôn đi kèm cờ đánh dấu nguồn gốc dữ liệu.
- Kiểm tra biên và ngoại lai: Phân biệt rõ ràng giữa giá thiếu (vắng mặt thông tin) với giá không dương (lỗi logic nghiệp vụ). Trên phân phối lệch phải mạnh, ngưỡng phân vị P99 mang lại khả năng khoanh vùng rà soát thực tế hơn nhiều so với ngưỡng tứ phân vị IQR.
- Bản chất của khóa tự nhiên: Tuyệt đối không nhầm lẫn giữa sự kiện độc lập trùng thuộc tính với bản ghi nhập lặp từ cơ sở dữ liệu. Không tự ý gọi
drop_duplicatestrên các thuộc tính không phải là khóa tự nhiên. - Đối soát cột dẫn xuất: Luôn kiểm tra định nghĩa biên thời gian và phạm vi tích lũy của nhà cung cấp trước khi vội vã đưa ra kết luận rằng cột số liệu bị sai sót.
- Chuẩn hóa báo cáo kiểm định: Duy trì đầy đủ các bài kiểm tra đạt chuẩn (kết quả bằng 0 vi phạm) để bảo đảm tính toàn diện và khả năng kiểm toán của hồ sơ dữ liệu.
Lab 11: Kiểm tra phản hồi của mô hình ngôn ngữ
Một phản hồi có đúng cấu trúc JSON vẫn có thể đọc sai cảm xúc của người viết. Ta sẽ kiểm riêng cấu trúc, độ khớp với nhãn tay và các dấu hiệu cần xem lại trên bộ phản hồi mô phỏng, không cần gọi dịch vụ trả phí.
Thông tin dữ liệu thực hành
Bộ bài tập sử dụng dữ liệu đánh giá chỗ ở Inside Airbnb với ảnh chụp ngày 29/06/2026 tại thành phố Santiago (Chile). Tệp dữ liệu gốc công bố tại địa chỉ mạng: https://data.insideairbnb.com/chile/rm/santiago/2026-06-29/data/reviews.csv.gz.
Nhằm phục vụ kiểm thử nhanh, độc lập môi trường và chạy trực tiếp không phụ thuộc kết nối mạng, các bài thực hành dưới đây sử dụng tập dữ liệu kiểm thử nhỏ gồm 240 bình luận giả lập DEMO_RV cho bài toán chọn mẫu, cùng bộ 10 đánh giá thực tế REVIEWS, tập nhãn tay chuẩn GOLD và tập phản hồi mô phỏng LLM_OUT có cài sẵn lỗi cấu trúc và ngữ nghĩa:
python
import json
import numpy as np
import pandas as pd
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
_MAU_CAU = [
"Great location, clean apartment and a very responsive host.",
"Buena ubicación y departamento limpio, volvería sin duda.",
"Ok",
".",
"Nice",
"Todo bien, gracias.",
"The apartment was noisy at night but the view was amazing.",
"Excelente anfitrión, muy atento a todo lo que necesitamos.",
"Muy bueno",
"Good value for money, close to the metro and supermarkets.",
]
DEMO_RV = pd.DataFrame(
{
"id": range(5000, 5240),
"comments": [f"{_MAU_CAU[k % 10]} (#{k})" for k in range(240)],
}
)
REVIEWS = {
1: "Place is great for a family, all clean, good location, bit noisy as the main avenue is behind, but we had a great time.",
2: "Apartment was irrelevant with pictures and not clean too so we cancelled our reservation. Alvaro helped us about cancellation process.",
3: "Cristian fue muy amable en todo momento, el lugar como se describia. La zona con muy buena movilidad. Muy recomendable. Gracias Cristian",
4: "Hermoso alojamiento! Lo pasamos re bien mi hija y yo. Es un poco ruidosa la zona si se abre la ventana. La vista es bella y esta muy bien ubicado. Sin duda volveriamos",
5: "Great location but apartment needs some attention to detail. Cable TV and wifi was out of service. Communication with host was poor. Bedding was not optimal.",
6: "Good WiFi, great location and centrally located. Shower was hot and had good pressure and there was enough space for two people for 6 days.",
7: "Es tal cual las fotos, buena ubicacion, tranquilo y sin ruido. Es en un piso 15 por si le temen a las alturas.",
8: ".",
9: "Nice place in cool region. Very noisy environment and apartment is not very clean.",
10: "location was great, wifi was spotty. But overall not a bad place.",
}
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
print(
f"Khởi tạo thành công {len(DEMO_RV)} bình luận giả lập và {len(REVIEWS)} đánh giá đối chiếu."
)Lưu ý: Mọi kết quả tính toán trên tập mẫu 240 dòng này là dữ liệu giả lập phục vụ kiểm chứng thuật toán, không đại diện cho quy mô và đặc trưng phân phối thực tế của toàn bộ thị trường Santiago.
Danh mục bài tập thực hành
Bài 1: Chọn mẫu bình luận tái lập được theo độ dài tối thiểu
Xây dựng hàm chọn mẫu ngẫu nhiên có kiểm soát hạt giống từ bảng dữ liệu bình luận sau khi đã áp dụng bộ lọc độ dài tối thiểu.
Yêu cầu kỹ thuật:
- Đầu vào: Bảng
rvkiểupd.DataFramechứa cộtcommentskiểu chuỗi, ngưỡng độ dài tối thiểudo_dai_minkiểu số nguyên (mặc định 50), số lượng mẫunkiểu số nguyên (mặc định 100), và hạt giống ngẫu nhiênseedkiểu số nguyên (mặc định 42). - Thao tác: Lọc các bản ghi có chiều dài văn bản lớn hơn hoặc bằng
do_dai_min, sau đó thực hiện lấy mẫu ngẫu nhiên đúngndòng thông qua phương thức.sample(n=n, random_state=seed). - Đầu ra: Trả về một
pd.DataFramechứa đầy đủ các cột và giữ nguyên chỉ số (index) gốc do phương thứcsamplesinh ra. - Trường hợp biên: Không làm thay đổi bảng dữ liệu đầu vào. Đưa ra thông báo hoặc xử lý khi số lượng bản ghi sau khi lọc nhỏ hơn số lượng mẫu yêu cầu
n. - Phân tích kỹ thuật: Trình bày hiện tượng thiên lệch chọn mẫu (selection bias) khi loại bỏ các bình luận quá ngắn.
Lời giải
Cách 1: Lọc dữ liệu qua phép lập chỉ mục boolean và lấy mẫu ngẫu nhiên
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
_MAU_CAU = [
"Great location, clean apartment and a very responsive host.",
"Buena ubicación y departamento limpio, volvería sin duda.",
"Ok",
".",
"Nice",
"Todo bien, gracias.",
"The apartment was noisy at night but the view was amazing.",
"Excelente anfitrión, muy atento a todo lo que necesitamos.",
"Muy bueno",
"Good value for money, close to the metro and supermarkets.",
]
DEMO_RV = pd.DataFrame(
{
"id": range(5000, 5240),
"comments": [f"{_MAU_CAU[k % 10]} (#{k})" for k in range(240)],
}
)
# Bắt đầu lời giải
def choose_sample(
rv: pd.DataFrame, do_dai_min: int = 50, n: int = 100, seed: int = 42
) -> pd.DataFrame:
"""Lọc các dòng có độ dài bình luận tối thiểu và lấy mẫu ngẫu nhiên có hạt giống.
Tham số rv: Bảng dữ liệu chứa cột comments.
Tham số do_dai_min: Ngưỡng ký tự tối thiểu của bình luận.
Tham số n: Số lượng dòng mẫu cần trích xuất.
Tham số seed: Hạt giống ngẫu nhiên nhằm bảo đảm tính tái lập.
"""
van_ban = rv.dropna(subset=["comments"]).copy()
van_ban["comments"] = van_ban["comments"].str.replace("<br/>", " ", regex=False)
loc_do_dai = van_ban.loc[van_ban["comments"].str.len().ge(do_dai_min)]
mau_ngau_nhien = loc_do_dai.sample(n=n, random_state=seed)
return mau_ngau_nhien
mau_co_ban = choose_sample(DEMO_RV, do_dai_min=50, n=100, seed=42)
print(
f"Cách 1: Đã lấy {len(mau_co_ban)} dòng, 3 ID đầu tiên: {mau_co_ban['id'].head(3).tolist()}"
)
# Kết thúc lời giảiGiải thích cách 1
Lọc dòng có văn bản, thay đúng thẻ xuống dòng được quy định rồi kiểm độ dài. sample dùng random_state để chọn lại cùng mẫu khi dữ liệu và phiên bản giữ nguyên. Kết quả chỉ đại diện cho miền văn bản qua bộ lọc, không tự đại diện cho mọi review.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Đã lấy 100 dòng, 3 ID đầu tiên: [5089, 5096, 5009] |
Cách 2: Kiểm soát ràng buộc số lượng bản ghi và trích xuất mẫu an toàn
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
_MAU_CAU = [
"Great location, clean apartment and a very responsive host.",
"Buena ubicación y departamento limpio, volvería sin duda.",
"Ok",
".",
"Nice",
"Todo bien, gracias.",
"The apartment was noisy at night but the view was amazing.",
"Excelente anfitrión, muy atento a todo lo que necesitamos.",
"Muy bueno",
"Good value for money, close to the metro and supermarkets.",
]
DEMO_RV = pd.DataFrame(
{
"id": range(5000, 5240),
"comments": [f"{_MAU_CAU[k % 10]} (#{k})" for k in range(240)],
}
)
def choose_sample(
rv: pd.DataFrame, do_dai_min: int = 50, n: int = 100, seed: int = 42
) -> pd.DataFrame:
"""Lọc các dòng có độ dài bình luận tối thiểu và lấy mẫu ngẫu nhiên có hạt giống.
Tham số rv: Bảng dữ liệu chứa cột comments.
Tham số do_dai_min: Ngưỡng ký tự tối thiểu của bình luận.
Tham số n: Số lượng dòng mẫu cần trích xuất.
Tham số seed: Hạt giống ngẫu nhiên nhằm bảo đảm tính tái lập.
"""
van_ban = rv.dropna(subset=["comments"]).copy()
van_ban["comments"] = van_ban["comments"].str.replace("<br/>", " ", regex=False)
loc_do_dai = van_ban.loc[van_ban["comments"].str.len().ge(do_dai_min)]
mau_ngau_nhien = loc_do_dai.sample(n=n, random_state=seed)
return mau_ngau_nhien
mau_co_ban = choose_sample(DEMO_RV, do_dai_min=50, n=100, seed=42)
# Bắt đầu lời giải
def choose_sample_robust(
rv: pd.DataFrame, do_dai_min: int = 50, n: int = 100, seed: int = 42
) -> pd.DataFrame:
"""Trích xuất mẫu có kiểm soát điều kiện biên và bảo toàn tính bất biến của bảng nguồn.
Tham số rv: Bảng dữ liệu nguồn cần trích xuất mẫu.
Tham số do_dai_min: Độ dài ký tự tối thiểu.
Tham số n: Số dòng cần lấy mẫu.
Tham số seed: Hạt giống sinh số ngẫu nhiên.
"""
if "comments" not in rv.columns:
raise KeyError("Bảng dữ liệu thiếu cột bắt buộc: comments")
van_ban = rv.dropna(subset=["comments"]).copy()
van_ban["comments"] = van_ban["comments"].str.replace("<br/>", " ", regex=False)
bang_loc = van_ban.loc[van_ban["comments"].str.len().ge(do_dai_min)]
if len(bang_loc) < n:
raise ValueError(
f"Số lượng bản ghi đạt chuẩn ({len(bang_loc)}) không đủ để lấy {n} mẫu."
)
return bang_loc.sample(n=n, random_state=seed).copy()
mau_chuan = choose_sample_robust(DEMO_RV, do_dai_min=50, n=100, seed=42)
print(
f"Cách 2: Đã lấy {len(mau_chuan)} dòng, kiểm tra trùng khớp: {mau_co_ban['id'].equals(mau_chuan['id'])}"
)
# Kết thúc lời giảiGiải thích cách 2
Kiểm số dòng đủ điều kiện trước khi lấy mẫu giúp báo lỗi có nghĩa thay vì âm thầm giảm cỡ mẫu. .copy() giữ vùng dữ liệu dùng để chuẩn hóa riêng. Ngưỡng độ dài có thể loại các nhận xét ngắn nhưng rõ ý, nên phải ghi bộ lọc khi báo chất lượng mô hình.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Đã lấy 100 dòng, kiểm tra trùng khớp: True |
So sánh cách dùng
Cách 1 giúp quan sát bộ lọc văn bản và việc lấy mẫu có hạt giống cố định. Cách 2 bổ sung bước kiểm tra số dòng đủ điều kiện để báo rõ khi không thể lấy đủ mẫu. Cả hai đều phải công bố ngưỡng độ dài đã dùng.
Bài 2: Ước lượng số lượng token và chi phí tính toán trước khi gọi mô hình
Xây dựng hàm ước lượng tổng số lượng token đầu vào và tổng chi phí tài chính tương ứng cho một tập hợp các chuỗi văn bản dựa trên quy tắc ngón tay cái và đơn giá giả định.
Yêu cầu kỹ thuật:
- Đầu vào: Cột văn bản
commentskiểupd.Serieschứa các chuỗi ký tự, và đơn giágia_moi_trieukiểu số thực biểu thị chi phí USD chotriệu token đầu vào (mặc định USD). - Thao tác: Áp dụng quy tắc ngón tay cái tính số token của mỗi chuỗi bằng phép chia nguyên cho
( len(cmt) // 4). Sau đó tính tổng số token trên toàn bộ các chuỗi. Chi phí được tính theo công thức: Gọi tổng số token ước lượng làvà đơn giá giả định trên một triệu token là . Chi phí ước lượng cùng đơn vị tiền với đơn giá:
- Đầu ra: Trả về một từ điển (
dict) gồm đúng hai khóa:"tong_token"kiểu số nguyên (int) và"chi_phi"kiểu số thực (float), không làm tròn. - Trường hợp biên: Xử lý an toàn khi Series đầu vào rỗng (trả về 0 token và 0.0 chi phí). Phép chia nguyên phải được thực hiện trên từng dòng riêng biệt trước khi tính tổng, không lấy tổng chiều dài rồi mới chia cho 4.
Lời giải
Cách 1: Duyệt vòng lặp tích lũy từng dòng văn bản
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
def estimate_cost(comments: pd.Series, gia_moi_trieu: float = 0.25) -> dict:
"""Ước lượng tổng số token và chi phí bằng vòng lặp cộng dồn từng dòng.
Tham số comments: Chuỗi các đoạn văn bản đánh giá.
Tham số gia_moi_trieu: Đơn giá USD cho một triệu token đầu vào.
"""
tong_token = 0
for cmt in comments:
tong_token += len(str(cmt)) // 4
chi_phi = tong_token * gia_moi_trieu / 1_000_000
return {"tong_token": int(tong_token), "chi_phi": float(chi_phi)}
vi_du_chuoi = pd.Series(["abcdefg", "abcd", "abc"])
kq_uoc_tinh = estimate_cost(vi_du_chuoi, gia_moi_trieu=0.25)
print(
f"Cách 1: Tổng token = {kq_uoc_tinh['tong_token']}, Chi phí = ${kq_uoc_tinh['chi_phi']:.8f}"
)
# Kết thúc lời giảiGiải thích cách 1
Đếm độ dài văn bản rồi dùng quy tắc ước lượng token của đề. Nhân tổng token với đơn giá giả định cho chi phí ước tính, không phải số tiền đã thanh toán. Ước lượng này chưa thay thế tokenizer hay hóa đơn của nhà cung cấp.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Tổng token = 2, Chi phí = $0.00000050 |
Cách 2: Tính toán vector hóa trên pandas Series
python
# Dữ liệu mẫu và các hàm hỗ trợ
import pandas as pd
# Bắt đầu lời giải
def estimate_cost_vectorized(comments: pd.Series, gia_moi_trieu: float = 0.25) -> dict:
"""Ước lượng token và chi phí thông qua biểu thức vector hóa của pandas.
Tham số comments: Series chứa văn bản đầu vào.
Tham số gia_moi_trieu: Đơn giá USD tính trên mỗi triệu token.
"""
if comments.empty:
return {"tong_token": 0, "chi_phi": 0.0}
tong_token = int((comments.astype(str).str.len() // 4).sum())
chi_phi = float(tong_token * gia_moi_trieu / 1_000_000)
return {"tong_token": tong_token, "chi_phi": chi_phi}
vi_du_chuoi = pd.Series(["abcdefg", "abcd", "abc"])
kq_mau_vector = estimate_cost_vectorized(vi_du_chuoi, gia_moi_trieu=0.25)
print(
f"Cách 2: Tổng token = {kq_mau_vector['tong_token']}, Chi phí = ${kq_mau_vector['chi_phi']:.8f}"
)
# Kết thúc lời giảiGiải thích cách 2
Tính theo Series làm rõ tổng ký tự và số bản ghi thay vì tích lũy thủ công. Tham số đơn giá cần giữ đơn vị trên một triệu token và thang tiền tệ đã nêu. Nếu thêm token đầu ra hoặc lời nhắc dùng chung, phải đưa chúng vào một giả thiết mới và báo riêng.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Tổng token = 2, Chi phí = $0.00000050 |
So sánh cách dùng
Hãy chọn Cách 1 khi bạn muốn theo dõi chi tiết độ dài ký tự và lượng token ước tính của từng phản hồi để kiểm chứng. Cách 2 phù hợp khi cần tính tổng chi phí dự kiến cho toàn bộ tập dữ liệu bằng các phép toán cột của pandas.
Bài 3: Thiết lập lược đồ ràng buộc dữ liệu đầu ra với Pydantic
Xây dựng lớp lược đồ dữ liệu ReviewInfo kế thừa từ BaseModel của thư viện Pydantic nhằm kiểm soát định dạng, kiểu dữ liệu và danh mục giá trị cho phép của kết quả phân tích cảm xúc từ mô hình ngôn ngữ lớn.
Yêu cầu kỹ thuật:
- Định nghĩa kiểu tập hợp giá trị khía cạnh:
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"] - Định nghĩa lớp
ReviewInfo(BaseModel)gồm đúng 4 trường bắt buộc, không có giá trị mặc định:sentiment: Nhận một trong ba giá trị thuộcLiteral["positive", "mixed", "negative"].aspects_positive: Danh sách các khía cạnh tích cực kiểulist[Aspect].aspects_negative: Danh sách các khía cạnh tiêu cực kiểulist[Aspect].language: Chuỗi ký tự định danh mã ngôn ngữ kiểustr.
- Hành vi kiểm định: Khi nạp dữ liệu hợp lệ, đối tượng được khởi tạo thành công và xuất dữ liệu thông qua phương thức
model_dump(). Khi gặp nhãn ngoài danh mục, thiếu trường bắt buộc, hoặc sai kiểu dữ liệu, hệ thống bắt buộc phải ném ngoại lệValidationError. Tuyệt đối không tự ý sửa đổi dữ liệu sai lệch.
Lời giải
Cách 1: Khai báo lớp Pydantic tiêu chuẩn với kiểu chú thích Type Annotations
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
# Bắt đầu lời giải
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
mau_hop_le_json = (
'{"sentiment": "mixed", "aspects_positive": ["location"], '
'"aspects_negative": ["noise"], "language": "en"}'
)
doi_tuong_hop_le = ReviewInfo.model_validate_json(mau_hop_le_json)
print(
f"Cách 1: Khởi tạo thành công, sentiment = {doi_tuong_hop_le.sentiment}, "
f"aspects_negative = {doi_tuong_hop_le.aspects_negative}"
)
# Kết thúc lời giảiGiải thích cách 1
Lược đồ định nghĩa các trường bắt buộc cùng miền nhãn cho cảm xúc, khía cạnh và ngôn ngữ. Kiểm từng trường giúp thấy phản hồi bị loại vì thiếu dữ liệu, sai kiểu hay nhãn ngoài danh mục. Một phản hồi qua lược đồ vẫn có thể gán cảm xúc sai so với văn bản.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Khởi tạo thành công, sentiment = mixed, aspects_negative = ['noise'] |
Cách 2: Khai báo lớp Pydantic nghiêm ngặt cấm trường thừa và chuẩn hóa chuỗi
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
# Bắt đầu lời giải
class ReviewInfoStrict(BaseModel):
"""Lược đồ dữ liệu với chế độ kiểm soát nghiêm ngặt ngăn chặn trường dư thừa."""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
try:
ReviewInfoStrict.model_validate_json(
'{"sentiment": "happy", "aspects_positive": [], "aspects_negative": [], "language": "en"}'
)
except ValidationError as loi_kiem_dinh:
print(
f"Cách 2: Chặn thành công nhãn ngoài danh mục. Chi tiết lỗi: {loi_kiem_dinh.errors()[0]['msg']}"
)
# Kết thúc lời giảiGiải thích cách 2
Pydantic gom quy tắc cấu trúc vào một mô hình có thể dùng lại. Literal giới hạn các nhãn được chấp nhận, còn cấu hình trường thừa và chế độ kiểm kiểu phải được đọc cùng nhau. Ngoại lệ kiểm tra cho biết quy tắc nào không khớp, không chứng minh mô hình đã đọc đúng ý nghĩa câu.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Chặn thành công nhãn ngoài danh mục. Chi tiết lỗi: Input should be 'positive', 'mixed' or 'negative' |
So sánh cách dùng
Kiểm từng trường trong Cách 1 giúp hiểu vì sao một phản hồi bị loại. Cách 2 gom các ràng buộc vào mô hình Pydantic để dùng lại và đọc lỗi kiểm tra. Với cả hai, đúng lược đồ chưa chứng minh nhãn cảm xúc đúng với câu nhận xét.
Bài 4: Kiểm định hàng loạt và phân tách bản ghi lỗi cấu trúc
Xây dựng hàm kiểm định hàng loạt các chuỗi phản hồi JSON từ mô hình ngôn ngữ lớn, phân tách độc lập các bản ghi đạt chuẩn vào nhóm hợp lệ và thu thập mã định danh của các bản ghi gặp lỗi lược đồ vào danh sách cảnh báo.
Yêu cầu kỹ thuật:
- Đầu vào: Từ điển
llm_outcó dạng{id: raw_json_str}, trong đó khóaidcó thể là số nguyên hoặc chuỗi ký tự. - Thao tác: Duyệt qua từng phần tử theo đúng thứ tự xuất hiện của từ điển. Gọi hàm kiểm định
ReviewInfo.model_validate_json(raw_json). Nếu hợp lệ, lưu đối tượngReviewInfovào từ điểnhop_le[id]. Nếu phát sinh ngoại lệValidationError(do nhãn ngoài danh mục, thiếu trường hoặc chuỗi JSON bị hỏng), lưu mã định danhidvào danh sáchloi_schema. - Đầu ra: Trả về một từ điển gồm đúng hai khóa:
"hop_le"(chứa từ điển các đối tượng đạt chuẩn) và"loi_schema"(chứa danh sách cácidbị chặn theo thứ tự ban đầu). - Trường hợp biên: Không thay đổi từ điển đầu vào. Khóa
iddạng chuỗi phải được giữ nguyên kiểu dữ liệu ban đầu. Chuỗi JSON hỏng cú pháp cũng phải được nhận diện và xếp vào danh sáchloi_schema.
Lời giải
Cách 1: Vòng lặp tường minh phân tách dữ liệu bằng khối try và except
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
# Bắt đầu lời giải
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
print(
f"Cách 1: Số lượng hợp lệ = {len(ket_qua_kiem_dinh['hop_le'])}, "
f"Danh sách ID lỗi lược đồ = {ket_qua_kiem_dinh['loi_schema']}"
)
# Kết thúc lời giảiGiải thích cách 1
Duyệt từng ID, giữ phản hồi hợp lệ và ghi riêng ID lỗi cấu trúc. Không để một phản hồi lỗi làm mất toàn bộ đợt đánh giá. Dữ liệu gốc cần được giữ để đọc lại nguyên nhân, thay vì chỉ giữ một tổng số lỗi.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Số lượng hợp lệ = 8, Danh sách ID lỗi lược đồ = [2, 5] |
Cách 2: Đóng gói nhật ký kiểm toán chi tiết nguyên nhân lỗi
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
# Bắt đầu lời giải
def validate_outputs_with_audit(llm_out: dict) -> dict:
"""Kiểm tra lược đồ hàng loạt kết hợp trích xuất nhật ký lỗi cấu trúc chi tiết.
Tham số llm_out: Từ điển chứa các phản hồi JSON thô.
"""
hop_le = {}
loi_schema = []
chi_tiet_loi = {}
for rid, raw_text in llm_out.items():
try:
hop_le[rid] = ReviewInfo.model_validate_json(raw_text)
except ValidationError as loi_val:
loi_schema.append(rid)
chi_tiet_loi[rid] = [
{
"truong": err["loc"],
"kieu_loi": err["type"],
"thong_diep": err["msg"],
}
for err in loi_val.errors()
]
except Exception as loi_khac:
loi_schema.append(rid)
chi_tiet_loi[rid] = [
{
"truong": ("root",),
"kieu_loi": "json_decode_error",
"thong_diep": str(loi_khac),
}
]
return {"hop_le": hop_le, "loi_schema": loi_schema, "chi_tiet_loi": chi_tiet_loi}
kq_kiem_toan = validate_outputs_with_audit(LLM_OUT)
print(
f"Cách 2: Ghi nhận {len(kq_kiem_toan['chi_tiet_loi'])} bản ghi lỗi với đầy đủ nhật ký kiểm toán."
)
# Kết thúc lời giảiGiải thích cách 2
Báo cáo kiểm tra có thể giữ ID, trạng thái và thông tin lỗi cho mỗi phản hồi. Việc tách phần hợp lệ khỏi phần bị loại giúp tính đúng mẫu số ở bước sau. Không gọi tỷ lệ hợp lệ là độ chính xác cảm xúc, vì đây là hai phép đo khác nhau.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Ghi nhận 2 bản ghi lỗi với đầy đủ nhật ký kiểm toán. |
So sánh cách dùng
Hãy chọn Cách 1 khi bạn chỉ cần tách nhanh các phản hồi hợp lệ để chuyển tiếp sang bước đánh giá mà không cần ghi nhận sâu lỗi. Cách 2 cần thiết khi phải lưu chi tiết mã lỗi và lý do không đạt chuẩn để phục vụ kiểm toán lời nhắc.
Bài 5: Đo lường độ chính xác phân loại cảm xúc so với nhãn tay chuẩn
Xây dựng hàm đo lường độ chính xác của nhãn cảm xúc do mô hình ngôn ngữ lớn dự đoán trên tập hợp các đầu ra hợp lệ bằng cách đối chiếu với tập nhãn tay chuẩn (gold standard).
Yêu cầu kỹ thuật:
- Đầu vào: Từ điển
du_doancó dạng{id: sentiment_str}chứa các nhãn do mô hình dự đoán (chỉ gồm các bản ghi đã qua kiểm định lược đồ, có ít nhất một phần tử), và từ điểngoldcó dạng{id: {"sentiment": str, ...}}chứa nhãn tay chuẩn tương ứng với mọiidtrongdu_doan. - Thao tác: So khớp giá trị
du_doan[id]vớigold[id]["sentiment"]. Đếm số lượng bản ghi có nhãn trùng khớp (so_dung), tính tỷ lệ chính xác (acc = so_dung / len(du_doan)), và thu thập danh sách cáciddự đoán sai theo đúng thứ tự xuất hiện củadu_doan. - Đầu ra: Trả về từ điển gồm đúng ba khóa:
"so_dung"kiểu số nguyên,"acc"kiểu số thực, và"sai"kiểu danh sách chứa cáciddự đoán không chính xác. - Phân tích kỹ thuật: Làm rõ sự khác biệt giữa mẫu số chỉ tính trên các bản ghi hợp lệ lược đồ (
bản ghi) và mẫu số tính trên toàn bộ tập dữ liệu ban đầu ( bản ghi).
Lời giải
Cách 1: So khớp tuần tự bằng phép lặp từ điển
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
tap_du_doan = {rid: obj.sentiment for rid, obj in ket_qua_kiem_dinh["hop_le"].items()}
# Bắt đầu lời giải
def sentiment_accuracy(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác của nhãn cảm xúc so với nhãn tay chuẩn.
Tham số du_doan: Từ điển nhãn cảm xúc dự đoán theo từng ID.
Tham số gold: Từ điển nhãn tay chuẩn của chuyên viên đánh giá.
"""
so_dung = 0
sai = []
for rid, nhan_du_doan in du_doan.items():
nhan_chuan = gold[rid]["sentiment"]
if nhan_du_doan == nhan_chuan:
so_dung += 1
else:
sai.append(rid)
do_chinh_xac = so_dung / len(du_doan) if du_doan else 0.0
return {"so_dung": so_dung, "acc": float(do_chinh_xac), "sai": sai}
tap_du_doan = {rid: obj.sentiment for rid, obj in ket_qua_kiem_dinh["hop_le"].items()}
kq_do_luong = sentiment_accuracy(tap_du_doan, GOLD)
print(
f"Cách 1: Số câu đúng = {kq_do_luong['so_dung']}/8, "
f"Độ chính xác = {kq_do_luong['acc']:.1%}, Các ID dự đoán sai = {kq_do_luong['sai']}"
)
# Kết thúc lời giảiGiải thích cách 1
Đối chiếu nhãn dự đoán với nhãn tay trên các ID cùng tồn tại. Đếm số khớp và số được đánh giá trước khi chia. Báo cả tỷ lệ trong tập hợp lệ lẫn tỷ lệ trên toàn mẫu khi đề yêu cầu, không che các phản hồi lỗi cấu trúc bằng cách bỏ chúng khỏi mẫu số mà không nói rõ.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Số câu đúng = 5/8, Độ chính xác = 62.5%, Các ID dự đoán sai = [4, 9, 10] |
Cách 2: Đối chiếu qua cấu trúc bảng pandas DataFrame
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
import pandas as pd
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
tap_du_doan = {rid: obj.sentiment for rid, obj in ket_qua_kiem_dinh["hop_le"].items()}
# Bắt đầu lời giải
def sentiment_accuracy_df(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác cảm xúc thông qua đối tượng bảng pandas DataFrame.
Tham số du_doan: Từ điển nhãn dự đoán.
Tham số gold: Từ điển nhãn chuẩn.
"""
records = []
for rid, pred in du_doan.items():
records.append({"id": rid, "pred": pred, "gold": gold[rid]["sentiment"]})
df_so_sanh = pd.DataFrame(records)
dung_mask = df_so_sanh["pred"] == df_so_sanh["gold"]
so_dung = int(dung_mask.sum())
acc = float(so_dung / len(df_so_sanh)) if len(df_so_sanh) > 0 else 0.0
sai = df_so_sanh.loc[~dung_mask, "id"].tolist()
return {"so_dung": so_dung, "acc": acc, "sai": sai}
kq_do_luong_df = sentiment_accuracy_df(tap_du_doan, GOLD)
print(
f"Cách 2: Xác nhận độ chính xác = {kq_do_luong_df['acc']:.3f}, Sai lệch tại ID: {kq_do_luong_df['sai']}"
)
# Kết thúc lời giảiGiải thích cách 2
Đưa nhãn dự đoán và tham chiếu thành hai cột giúp nhìn trực tiếp các ID không khớp. Phải căn theo ID, không ghép theo vị trí vì thứ tự hai dict hoặc hai bảng có thể khác. Nhãn tay cũng cần quy tắc gán nhất quán, nên đây là độ khớp với bộ tham chiếu đã cho.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Xác nhận độ chính xác = 0.625, Sai lệch tại ID: [4, 9, 10] |
So sánh cách dùng
Bạn nên chọn Cách 1 khi dữ liệu ở dạng từ điển thuần túy và chỉ muốn tính nhanh tỷ lệ chính xác trên tập chung. Cách 2 trực quan hơn khi bạn cần đưa hai nguồn nhãn vào cùng một bảng để dễ quan sát và lọc các trường hợp gán sai.
Bài 6: Phân loại lỗi ngữ nghĩa và quy chuẩn hóa hướng dẫn gán nhãn
Xây dựng hàm phân loại các trường hợp dự đoán sai thành các nhóm nguyên nhân ngữ nghĩa đặc thù dựa trên ngữ cảnh thực tế của văn bản đánh giá, đồng thời thiết lập cấu trúc bảng đối chiếu phục vụ cải tiến lời nhắc (prompt engineering) và hướng dẫn gán nhãn.
Yêu cầu kỹ thuật:
- Đầu vào: Danh sách mã lỗi
sai_idskiểulist, từ điển bài đánh giá gốcreviewskiểudict, từ điển nhãn dự đoánpredictionskiểudict, và từ điển nhãn chuẩngoldkiểudict. - Thao tác: Trích xuất nội dung văn bản gốc cùng cặp nhãn đối lập cho từng ID bị gán sai (ID 4, 9, 10). Phân loại chính xác bản chất lỗi theo ba nhóm nguyên nhân:
- Bỏ sót ý chê do lời khen lấn át (ID 4).
- Bất đồng ranh giới phân loại do tính chất phức hợp (ID 9).
- Hiểu sai câu phủ định ngữ dụng (ID 10).
- Đầu ra: Trả về một
pd.DataFramechứa các cộtid,gold,llm,kieu_loi,ngu_canh_ngan. - Phân tích kỹ thuật: Giải thích vai trò của tài liệu Hướng dẫn gán nhãn (Annotation Guidelines) như một mốc tham chiếu thống nhất khi người gán nhãn xuất hiện bất đồng quan điểm.
Lời giải
Cách 1: Ánh xạ quy tắc phân loại lỗi qua từ điển danh mục
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
import pandas as pd
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
tap_du_doan = {rid: obj.sentiment for rid, obj in ket_qua_kiem_dinh["hop_le"].items()}
def sentiment_accuracy(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác của nhãn cảm xúc so với nhãn tay chuẩn.
Tham số du_doan: Từ điển nhãn cảm xúc dự đoán theo từng ID.
Tham số gold: Từ điển nhãn tay chuẩn của chuyên viên đánh giá.
"""
so_dung = 0
sai = []
for rid, nhan_du_doan in du_doan.items():
nhan_chuan = gold[rid]["sentiment"]
if nhan_du_doan == nhan_chuan:
so_dung += 1
else:
sai.append(rid)
do_chinh_xac = so_dung / len(du_doan) if du_doan else 0.0
return {"so_dung": so_dung, "acc": float(do_chinh_xac), "sai": sai}
kq_do_luong = sentiment_accuracy(tap_du_doan, GOLD)
REVIEWS = {
1: "Place is great for a family, all clean, good location, bit noisy as the main avenue is behind, but we had a great time.",
2: "Apartment was irrelevant with pictures and not clean too so we cancelled our reservation. Alvaro helped us about cancellation process.",
3: "Cristian fue muy amable en todo momento, el lugar como se describia. La zona con muy buena movilidad. Muy recomendable. Gracias Cristian",
4: "Hermoso alojamiento! Lo pasamos re bien mi hija y yo. Es un poco ruidosa la zona si se abre la ventana. La vista es bella y esta muy bien ubicado. Sin duda volveriamos",
5: "Great location but apartment needs some attention to detail. Cable TV and wifi was out of service. Communication with host was poor. Bedding was not optimal.",
6: "Good WiFi, great location and centrally located. Shower was hot and had good pressure and there was enough space for two people for 6 days.",
7: "Es tal cual las fotos, buena ubicacion, tranquilo y sin ruido. Es en un piso 15 por si le temen a las alturas.",
8: ".",
9: "Nice place in cool region. Very noisy environment and apartment is not very clean.",
10: "location was great, wifi was spotty. But overall not a bad place.",
}
# Bắt đầu lời giải
def diagnose_semantic_errors(
sai_ids: list, reviews: dict, predictions: dict, gold: dict
) -> pd.DataFrame:
"""Tạo bảng phân tích và phân loại bản chất lỗi ngữ nghĩa của các ca dự đoán sai.
Tham số sai_ids: Danh sách các ID dự đoán sai.
Tham số reviews: Từ điển văn bản đánh giá gốc.
Tham số predictions: Từ điển nhãn dự đoán của mô hình.
Tham số gold: Từ điển nhãn chuẩn của chuyên viên.
"""
danh_muc_loi = {
4: "Bỏ sót ý chê: Lời khen dài che khuất nhận xét tiêu cực về tiếng ồn",
9: "Ranh giới nhập nhằng: Nhận xét khen địa điểm nhưng chê tiếng ồn và vệ sinh",
10: "Hiểu sai phủ định: Cụm từ not a bad place mang nghĩa khen nhẹ nhưng bị đọc thành chê",
}
dong_du_lieu = []
for rid in sai_ids:
dong_du_lieu.append(
{
"id": rid,
"gold": gold[rid]["sentiment"],
"llm": predictions[rid],
"kieu_loi": danh_muc_loi.get(rid, "Lỗi phân loại khác"),
"ngu_canh_ngan": reviews[rid][:60] + "...",
}
)
return pd.DataFrame(dong_du_lieu)
df_phan_loai = diagnose_semantic_errors(kq_do_luong["sai"], REVIEWS, tap_du_doan, GOLD)
print(f"Cách 1: Đã phân loại {len(df_phan_loai)} trường hợp lỗi ngữ nghĩa điển hình.")
# Kết thúc lời giảiGiải thích cách 1
Đọc nguyên văn các trường hợp không khớp trước khi đặt tên lỗi. Câu có cả khen và chê, phủ định hoặc khác biệt cách hiểu cần được mô tả bằng bằng chứng ngôn ngữ. Không suy ra nguyên nhân bên trong mô hình chỉ từ một nhãn sai.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Đã phân loại 3 trường hợp lỗi ngữ nghĩa điển hình. |
Cách 2: Tự động nhận diện cấu trúc ngữ nghĩa qua từ khóa đối lập và phủ định
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
import pandas as pd
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
tap_du_doan = {rid: obj.sentiment for rid, obj in ket_qua_kiem_dinh["hop_le"].items()}
def sentiment_accuracy(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác của nhãn cảm xúc so với nhãn tay chuẩn.
Tham số du_doan: Từ điển nhãn cảm xúc dự đoán theo từng ID.
Tham số gold: Từ điển nhãn tay chuẩn của chuyên viên đánh giá.
"""
so_dung = 0
sai = []
for rid, nhan_du_doan in du_doan.items():
nhan_chuan = gold[rid]["sentiment"]
if nhan_du_doan == nhan_chuan:
so_dung += 1
else:
sai.append(rid)
do_chinh_xac = so_dung / len(du_doan) if du_doan else 0.0
return {"so_dung": so_dung, "acc": float(do_chinh_xac), "sai": sai}
kq_do_luong = sentiment_accuracy(tap_du_doan, GOLD)
REVIEWS = {
1: "Place is great for a family, all clean, good location, bit noisy as the main avenue is behind, but we had a great time.",
2: "Apartment was irrelevant with pictures and not clean too so we cancelled our reservation. Alvaro helped us about cancellation process.",
3: "Cristian fue muy amable en todo momento, el lugar como se describia. La zona con muy buena movilidad. Muy recomendable. Gracias Cristian",
4: "Hermoso alojamiento! Lo pasamos re bien mi hija y yo. Es un poco ruidosa la zona si se abre la ventana. La vista es bella y esta muy bien ubicado. Sin duda volveriamos",
5: "Great location but apartment needs some attention to detail. Cable TV and wifi was out of service. Communication with host was poor. Bedding was not optimal.",
6: "Good WiFi, great location and centrally located. Shower was hot and had good pressure and there was enough space for two people for 6 days.",
7: "Es tal cual las fotos, buena ubicacion, tranquilo y sin ruido. Es en un piso 15 por si le temen a las alturas.",
8: ".",
9: "Nice place in cool region. Very noisy environment and apartment is not very clean.",
10: "location was great, wifi was spotty. But overall not a bad place.",
}
# Bắt đầu lời giải
def analyze_error_patterns(
sai_ids: list, reviews: dict, predictions: dict, gold: dict
) -> pd.DataFrame:
"""Phân tích mẫu lỗi tự động dựa trên dấu hiệu cú pháp và liên từ tương phản.
Tham số sai_ids: Danh sách mã định danh sai.
Tham số reviews: Bảng tra cứu văn bản gốc.
Tham số predictions: Nhãn mô hình gán.
Tham số gold: Nhãn chuẩn tham chiếu.
"""
ket_qua = []
for rid in sai_ids:
text_lower = reviews[rid].lower()
if "not a bad" in text_lower or "not bad" in text_lower:
loai = "Hiểu sai phủ định ngữ dụng (Litotes)"
elif (
any(kw in text_lower for kw in ["ruidosa", "noisy"])
and predictions[rid] == "positive"
):
loai = "Bỏ sót khía cạnh tiếng ồn do thiên lệch từ ngữ tích cực"
else:
loai = "Xung đột trọng số giữa các khía cạnh khen và chê"
ket_qua.append(
{
"id": rid,
"gold": gold[rid]["sentiment"],
"llm": predictions[rid],
"mau_ngu_nghia": loai,
}
)
return pd.DataFrame(ket_qua)
df_mau_loi = analyze_error_patterns(kq_do_luong["sai"], REVIEWS, tap_du_doan, GOLD)
print(
f"Cách 2: Phân tích mẫu ngữ nghĩa tự động:\n{df_mau_loi[['id', 'mau_ngu_nghia']].to_string(index=False)}"
)
# Kết thúc lời giảiGiải thích cách 2
Các từ khóa có thể gợi ý một loại lỗi để người đọc xem lại, nhưng không tự phân loại ngữ nghĩa đầy đủ. Bảng đối chiếu giữ văn bản, nhãn dự đoán, nhãn tay và ghi chú làm bằng chứng. Khi sửa hướng dẫn gán nhãn, kiểm lại trên dữ liệu khác thay vì chỉ tối ưu cho vài câu đã thấy.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Phân tích mẫu ngữ nghĩa tự động: |
| 2 | id mau_ngu_nghia |
| 3 | 4 Bỏ sót khía cạnh tiếng ồn do thiên lệch từ ngữ tích cực |
| 4 | 9 Xung đột trọng số giữa các khía cạnh khen và chê |
| 5 | 10 Hiểu sai phủ định ngữ dụng (Litotes) |
So sánh cách dùng
Cách 1 thích hợp để đọc và giải thích từng trường hợp sai theo ngữ cảnh. Cách 2 dùng dấu hiệu từ khóa để chọn các câu cần xem lại. Kết quả sàng lọc này hỗ trợ việc đọc, không tự thay thế đánh giá ý nghĩa của câu.
Bài 7: Thiết lập quy tắc hậu kiểm tự động phát hiện mâu thuẫn nội tại
Xây dựng hàm hậu kiểm tự động kiểm tra tính nhất quán logic giữa nhãn cảm xúc tổng thể và các danh sách khía cạnh chi tiết, gắn cờ các bản ghi tự mâu thuẫn mà không cần sử dụng nhãn tay đối chứng.
Yêu cầu kỹ thuật:
- Đầu vào: Từ điển
outputscó dạng{id: {"sentiment": str, "aspects_positive": list, "aspects_negative": list}}(ví dụ từ phương thứcmodel_dump()của các đối tượng hợp lệ). - Thao tác: Kiểm tra quy tắc mâu thuẫn nội tại:
- Nếu
sentiment == "positive"nhưng danh sáchaspects_negativekhông rỗng: Vi phạm mâu thuẫn. - Nếu
sentiment == "negative"nhưng danh sáchaspects_positivekhông rỗng: Vi phạm mâu thuẫn. - Nếu
sentiment == "mixed": Không bao giờ vi phạm quy tắc này.
- Nếu
- Đầu ra: Trả về danh sách (
list) chứa cácidbị gắn cờ mâu thuẫn, giữ đúng thứ tự xuất hiện của từ điển ban đầu. - Phân tích kỹ thuật: Đối chiếu danh sách các ID bị gắn cờ với các ca sai lệch thực tế ở Bài 5, làm rõ ưu điểm và giới hạn của quy tắc hậu kiểm tự động.
Lời giải
Cách 1: Kiểm tra cấu trúc rẽ nhánh điều kiện tuần tự
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
tap_dau_ra_dump = {
rid: obj.model_dump() for rid, obj in ket_qua_kiem_dinh["hop_le"].items()
}
# Bắt đầu lời giải
def contradictions(outputs: dict) -> list:
"""Phát hiện các bản ghi tự mâu thuẫn giữa nhãn cảm xúc và danh sách khía cạnh.
Tham số outputs: Từ điển các đối tượng thông tin đánh giá đã xuất dữ liệu.
"""
danh_sach_vi_pham = []
for rid, data in outputs.items():
sentiment = data.get("sentiment")
pos_aspects = data.get("aspects_positive", [])
neg_aspects = data.get("aspects_negative", [])
if sentiment == "positive" and len(neg_aspects) > 0:
danh_sach_vi_pham.append(rid)
elif sentiment == "negative" and len(pos_aspects) > 0:
danh_sach_vi_pham.append(rid)
return danh_sach_vi_pham
tap_dau_ra_dump = {
rid: obj.model_dump() for rid, obj in ket_qua_kiem_dinh["hop_le"].items()
}
ds_mau_thuan = contradictions(tap_dau_ra_dump)
print(
f"Cách 1: Quy tắc hậu kiểm phát hiện {len(ds_mau_thuan)} bản ghi mâu thuẫn: {ds_mau_thuan}"
)
# Kết thúc lời giảiGiải thích cách 1
Kiểm những cặp nhãn có vẻ mâu thuẫn theo quy tắc đã định, chẳng hạn cảm xúc tích cực đi cùng khía cạnh chê. Danh sách ID gắn cờ là hàng đợi đọc lại, không phải toàn bộ các lỗi. Một câu có cảm xúc hỗn hợp hoặc nhận xét nhiều mặt có thể làm quy tắc đơn giản báo nhầm.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Quy tắc hậu kiểm phát hiện 2 bản ghi mâu thuẫn: [9, 10] |
Cách 2: Biểu thức kiểm định logic hàm mở rộng
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
ket_qua_kiem_dinh = validate_outputs(LLM_OUT)
tap_dau_ra_dump = {
rid: obj.model_dump() for rid, obj in ket_qua_kiem_dinh["hop_le"].items()
}
def contradictions(outputs: dict) -> list:
"""Phát hiện các bản ghi tự mâu thuẫn giữa nhãn cảm xúc và danh sách khía cạnh.
Tham số outputs: Từ điển các đối tượng thông tin đánh giá đã xuất dữ liệu.
"""
danh_sach_vi_pham = []
for rid, data in outputs.items():
sentiment = data.get("sentiment")
pos_aspects = data.get("aspects_positive", [])
neg_aspects = data.get("aspects_negative", [])
if sentiment == "positive" and len(neg_aspects) > 0:
danh_sach_vi_pham.append(rid)
elif sentiment == "negative" and len(pos_aspects) > 0:
danh_sach_vi_pham.append(rid)
return danh_sach_vi_pham
ds_mau_thuan = contradictions(tap_dau_ra_dump)
# Bắt đầu lời giải
def is_contradictory_record(record: dict) -> bool:
"""Xác định một bản ghi đơn lẻ có vi phạm tính nhất quán logic hay không."""
sentiment = record.get("sentiment")
if sentiment == "positive" and bool(record.get("aspects_negative")):
return True
if sentiment == "negative" and bool(record.get("aspects_positive")):
return True
return False
def contradictions_functional(outputs: dict) -> list:
"""Lọc các ID vi phạm quy tắc hậu kiểm bằng hàm vị từ logic.
Tham số outputs: Từ điển dữ liệu đầu ra cần thẩm định.
"""
return [rid for rid, rec in outputs.items() if is_contradictory_record(rec)]
ds_mau_thuan_fn = contradictions_functional(tap_dau_ra_dump)
print(
f"Cách 2: Xác nhận danh sách mâu thuẫn trùng khớp: {ds_mau_thuan == ds_mau_thuan_fn}"
)
# Kết thúc lời giảiGiải thích cách 2
Tách hàm kiểm từng phản hồi khỏi thao tác lọc cả đợt giúp dùng lại cùng điều kiện. Quy tắc hậu kiểm không thay thế nhãn tham chiếu. Cần đo cả trường hợp bị gắn cờ đúng, gắn cờ nhầm và lỗi không bị gắn cờ nếu muốn đánh giá chất lượng của chính bộ kiểm.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Xác nhận danh sách mâu thuẫn trùng khớp: True |
So sánh cách dùng
Bạn nên chọn Cách 1 khi chỉ cần kiểm tra vài cặp mâu thuẫn đơn giản để đưa vào danh sách xem xét lại thủ công. Cách 2 phù hợp khi có bộ quy tắc phức tạp cần đóng gói thành hàm kiểm định riêng biệt để dễ bảo trì và mở rộng.
Bài 8: Đối chiếu phản hồi mô hình thực nghiệm mở rộng với bộ mô phỏng
Thiết lập quy trình đánh giá toàn diện một bộ phản hồi mô hình mới (LLM_RUN_2) đã được tinh chỉnh lời nhắc nhằm khắc phục lỗi cấu trúc và ngữ nghĩa, đối chiếu các chỉ số chất lượng giữa hai đợt chạy mà không cần sử dụng khóa kết nối API thực tế.
Yêu cầu kỹ thuật:
- Đầu vào: Bộ phản hồi thứ hai
LLM_RUN_2gồm 10 bản ghi JSON mô phỏng, tập nhãn tayGOLDvà bộ phản hồi ban đầuLLM_OUT. - Thao tác:
- Thẩm định tính hợp lệ của lược đồ trên toàn bộ 10 bản ghi của
LLM_RUN_2. - Đo lường độ chính xác phân loại cảm xúc so với
GOLDtrên cả tập hợp lệ và trên toàn bộ tập dữ liệu. - Kiểm tra xem ca phủ định tại ID 10 ("not a bad place") có được phân loại chính xác hay không.
- Chạy quy tắc hậu kiểm tự động để xác nhận trạng thái mâu thuẫn nội tại.
- Thẩm định tính hợp lệ của lược đồ trên toàn bộ 10 bản ghi của
- Đầu ra: Trả về từ điển báo cáo tổng hợp hoặc bảng
pd.DataFrameđối chiếu các chỉ số giữa hai đợt chạy. - Phân tích kỹ thuật: Nêu rõ ý nghĩa của việc lưu vết phản hồi thô (raw output trace) và quy trình kiểm thử hồi quy (regression testing) khi nâng cấp mô hình hoặc sửa đổi lời nhắc.
Lời giải
Cách 1: Thực thi tuần tự các hàm thẩm định và so sánh kết quả trực tiếp
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
def sentiment_accuracy(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác của nhãn cảm xúc so với nhãn tay chuẩn.
Tham số du_doan: Từ điển nhãn cảm xúc dự đoán theo từng ID.
Tham số gold: Từ điển nhãn tay chuẩn của chuyên viên đánh giá.
"""
so_dung = 0
sai = []
for rid, nhan_du_doan in du_doan.items():
nhan_chuan = gold[rid]["sentiment"]
if nhan_du_doan == nhan_chuan:
so_dung += 1
else:
sai.append(rid)
do_chinh_xac = so_dung / len(du_doan) if du_doan else 0.0
return {"so_dung": so_dung, "acc": float(do_chinh_xac), "sai": sai}
def contradictions(outputs: dict) -> list:
"""Phát hiện các bản ghi tự mâu thuẫn giữa nhãn cảm xúc và danh sách khía cạnh.
Tham số outputs: Từ điển các đối tượng thông tin đánh giá đã xuất dữ liệu.
"""
danh_sach_vi_pham = []
for rid, data in outputs.items():
sentiment = data.get("sentiment")
pos_aspects = data.get("aspects_positive", [])
neg_aspects = data.get("aspects_negative", [])
if sentiment == "positive" and len(neg_aspects) > 0:
danh_sach_vi_pham.append(rid)
elif sentiment == "negative" and len(pos_aspects) > 0:
danh_sach_vi_pham.append(rid)
return danh_sach_vi_pham
# Bắt đầu lời giải
LLM_RUN_2 = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": [], "aspects_negative": ["amenities", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["noise"], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": [], "aspects_negative": ["amenities", "host"], "language": "en"}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def evaluate_llm_run(raw_data: dict, gold_data: dict) -> dict:
"""Đánh giá toàn diện một đợt chạy của mô hình ngôn ngữ lớn.
Tham số raw_data: Tập hợp chuỗi JSON thô do mô hình trả về.
Tham số gold_data: Tập nhãn tay chuẩn.
"""
kiem_dinh = validate_outputs(raw_data)
hop_le = kiem_dinh["hop_le"]
loi_schema = kiem_dinh["loi_schema"]
du_doan = {rid: obj.sentiment for rid, obj in hop_le.items()}
ket_qua_acc = (
sentiment_accuracy(du_doan, gold_data)
if du_doan
else {"so_dung": 0, "acc": 0.0, "sai": []}
)
dump_data = {rid: obj.model_dump() for rid, obj in hop_le.items()}
ds_mau_thuan = contradictions(dump_data)
acc_toan_bo = ket_qua_acc["so_dung"] / len(raw_data) if raw_data else 0.0
return {
"tong_so": len(raw_data),
"hop_le_count": len(hop_le),
"loi_schema_count": len(loi_schema),
"ty_le_hop_le": len(hop_le) / len(raw_data) if raw_data else 0.0,
"acc_hop_le": ket_qua_acc["acc"],
"acc_toan_bo": acc_toan_bo,
"sai_ids": ket_qua_acc["sai"],
"mau_thuan_ids": ds_mau_thuan,
"id_10_status": (
"Chính xác"
if 10 in hop_le and hop_le[10].sentiment == gold_data[10]["sentiment"]
else "Sai"
),
}
bc_run_1 = evaluate_llm_run(LLM_OUT, GOLD)
bc_run_2 = evaluate_llm_run(LLM_RUN_2, GOLD)
print(
f"Cách 1: Run 1 Acc (Hợp lệ) = {bc_run_1['acc_hop_le']:.1%}, Run 2 Acc (Hợp lệ) = {bc_run_2['acc_hop_le']:.1%}"
)
# Kết thúc lời giảiGiải thích cách 1
Đưa cùng tập nhãn tay vào mỗi lần đánh giá để so sánh các tỷ lệ trên cùng mẫu. Giữ tỷ lệ hợp lệ, độ khớp trong tập hợp lệ và độ khớp toàn mẫu thành các trường riêng. Kết quả của đợt phản hồi mô phỏng tốt hơn không chứng minh một lời nhắc mới sẽ luôn tốt hơn trên dữ liệu thực.
Kết quả cách 1
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 1: Run 1 Acc (Hợp lệ) = 62.5%, Run 2 Acc (Hợp lệ) = 100.0% |
Cách 2: Đóng gói đường ống đánh giá và xuất báo cáo so sánh đa tiêu chí
python
# Dữ liệu mẫu và các hàm hỗ trợ
from pydantic import BaseModel, ValidationError, ConfigDict
from typing import Literal
import pandas as pd
Aspect = Literal["location", "cleanliness", "host", "noise", "amenities", "value"]
class ReviewInfo(BaseModel):
"""Lược đồ dữ liệu trích xuất thông tin đánh giá chỗ ở.
Trường sentiment: Nhãn cảm xúc tổng thể gồm positive, mixed, hoặc negative.
Trường aspects_positive: Danh sách khía cạnh tích cực được đề cập.
Trường aspects_negative: Danh sách khía cạnh tiêu cực được đề cập.
Trường language: Mã ngôn ngữ của bài đánh giá.
"""
sentiment: Literal["positive", "mixed", "negative"]
aspects_positive: list[Aspect]
aspects_negative: list[Aspect]
language: str
LLM_OUT = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": ["host"], "aspects_negative": ["photos", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities", "host"]}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "negative", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
GOLD = {
1: {"sentiment": "mixed", "language": "en"},
2: {"sentiment": "negative", "language": "en"},
3: {"sentiment": "positive", "language": "es"},
4: {"sentiment": "mixed", "language": "es"},
5: {"sentiment": "negative", "language": "en"},
6: {"sentiment": "positive", "language": "en"},
7: {"sentiment": "positive", "language": "es"},
8: {"sentiment": "mixed", "language": "und"},
9: {"sentiment": "mixed", "language": "en"},
10: {"sentiment": "mixed", "language": "en"},
}
def validate_outputs(llm_out: dict) -> dict:
"""Kiểm tra hợp lệ hàng loạt các phản hồi JSON bằng lược đồ ReviewInfo.
Tham số llm_out: Từ điển chứa cặp khóa id và giá trị chuỗi JSON thô.
"""
hop_le = {}
loi_schema = []
for rid, raw_text in llm_out.items():
try:
doi_tuong = ReviewInfo.model_validate_json(raw_text)
hop_le[rid] = doi_tuong
except (ValidationError, Exception):
loi_schema.append(rid)
return {"hop_le": hop_le, "loi_schema": loi_schema}
def sentiment_accuracy(du_doan: dict, gold: dict) -> dict:
"""Tính độ chính xác của nhãn cảm xúc so với nhãn tay chuẩn.
Tham số du_doan: Từ điển nhãn cảm xúc dự đoán theo từng ID.
Tham số gold: Từ điển nhãn tay chuẩn của chuyên viên đánh giá.
"""
so_dung = 0
sai = []
for rid, nhan_du_doan in du_doan.items():
nhan_chuan = gold[rid]["sentiment"]
if nhan_du_doan == nhan_chuan:
so_dung += 1
else:
sai.append(rid)
do_chinh_xac = so_dung / len(du_doan) if du_doan else 0.0
return {"so_dung": so_dung, "acc": float(do_chinh_xac), "sai": sai}
def contradictions(outputs: dict) -> list:
"""Phát hiện các bản ghi tự mâu thuẫn giữa nhãn cảm xúc và danh sách khía cạnh.
Tham số outputs: Từ điển các đối tượng thông tin đánh giá đã xuất dữ liệu.
"""
danh_sach_vi_pham = []
for rid, data in outputs.items():
sentiment = data.get("sentiment")
pos_aspects = data.get("aspects_positive", [])
neg_aspects = data.get("aspects_negative", [])
if sentiment == "positive" and len(neg_aspects) > 0:
danh_sach_vi_pham.append(rid)
elif sentiment == "negative" and len(pos_aspects) > 0:
danh_sach_vi_pham.append(rid)
return danh_sach_vi_pham
LLM_RUN_2 = {
1: '{"sentiment": "mixed", "aspects_positive": ["cleanliness", "location"], "aspects_negative": ["noise"], "language": "en"}',
2: '{"sentiment": "negative", "aspects_positive": [], "aspects_negative": ["amenities", "cleanliness"], "language": "en"}',
3: '{"sentiment": "positive", "aspects_positive": ["host", "location"], "aspects_negative": [], "language": "es"}',
4: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["noise"], "language": "es"}',
5: '{"sentiment": "negative", "aspects_positive": [], "aspects_negative": ["amenities", "host"], "language": "en"}',
6: '{"sentiment": "positive", "aspects_positive": ["amenities", "location"], "aspects_negative": [], "language": "en"}',
7: '{"sentiment": "positive", "aspects_positive": ["location"], "aspects_negative": [], "language": "es"}',
8: '{"sentiment": "mixed", "aspects_positive": [], "aspects_negative": [], "language": "und"}',
9: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["noise", "cleanliness"], "language": "en"}',
10: '{"sentiment": "mixed", "aspects_positive": ["location"], "aspects_negative": ["amenities"], "language": "en"}',
}
def evaluate_llm_run(raw_data: dict, gold_data: dict) -> dict:
"""Đánh giá toàn diện một đợt chạy của mô hình ngôn ngữ lớn.
Tham số raw_data: Tập hợp chuỗi JSON thô do mô hình trả về.
Tham số gold_data: Tập nhãn tay chuẩn.
"""
kiem_dinh = validate_outputs(raw_data)
hop_le = kiem_dinh["hop_le"]
loi_schema = kiem_dinh["loi_schema"]
du_doan = {rid: obj.sentiment for rid, obj in hop_le.items()}
ket_qua_acc = (
sentiment_accuracy(du_doan, gold_data)
if du_doan
else {"so_dung": 0, "acc": 0.0, "sai": []}
)
dump_data = {rid: obj.model_dump() for rid, obj in hop_le.items()}
ds_mau_thuan = contradictions(dump_data)
acc_toan_bo = ket_qua_acc["so_dung"] / len(raw_data) if raw_data else 0.0
return {
"tong_so": len(raw_data),
"hop_le_count": len(hop_le),
"loi_schema_count": len(loi_schema),
"ty_le_hop_le": len(hop_le) / len(raw_data) if raw_data else 0.0,
"acc_hop_le": ket_qua_acc["acc"],
"acc_toan_bo": acc_toan_bo,
"sai_ids": ket_qua_acc["sai"],
"mau_thuan_ids": ds_mau_thuan,
"id_10_status": (
"Chính xác"
if 10 in hop_le and hop_le[10].sentiment == gold_data[10]["sentiment"]
else "Sai"
),
}
# Bắt đầu lời giải
def evaluate_llm_pipeline(run_dict: dict, gold_data: dict) -> pd.DataFrame:
"""Tạo bảng so sánh đối chuẩn hiệu năng giữa nhiều đợt chạy mô hình khác nhau.
Tham số run_dict: Từ điển chứa tên đợt chạy và bộ phản hồi thô tương ứng.
Tham số gold_data: Tập nhãn tay chuẩn đối chiếu.
"""
hang_bao_cao = []
for ten_run, raw_dict in run_dict.items():
so_lieu = evaluate_llm_run(raw_dict, gold_data)
hang_bao_cao.append(
{
"Dot_chay": ten_run,
"Tong_mau": so_lieu["tong_so"],
"Loi_schema": so_lieu["loi_schema_count"],
"Ty_le_schema": f"{so_lieu['ty_le_hop_le']:.1%}",
"Acc_hop_le": f"{so_lieu['acc_hop_le']:.1%}",
"Acc_toan_bo": f"{so_lieu['acc_toan_bo']:.1%}",
"So_mau_thuan": len(so_lieu["mau_thuan_ids"]),
"Xu_ly_ID_10": so_lieu["id_10_status"],
}
)
return pd.DataFrame(hang_bao_cao)
df_so_sanh_runs = evaluate_llm_pipeline(
{"Run 1 (Mô phỏng gốc)": LLM_OUT, "Run 2 (Cải tiến lời nhắc)": LLM_RUN_2}, GOLD
)
print(
f"Cách 2: Bảng đối chuẩn kết quả giữa hai đợt chạy:\n{df_so_sanh_runs.to_string(index=False)}"
)
# Kết thúc lời giảiGiải thích cách 2
Gom các phép kiểm thành một hàm báo cáo giúp chạy lại cùng tiêu chí trên nhiều bộ phản hồi đã lưu. So sánh theo ID và giữ nguyên đầu ra thô để có thể kiểm lại. Chênh lệch hai tỷ lệ phần trăm được trình bày bằng điểm phần trăm, còn mức tăng tương đối dùng một phép chia khác.
Kết quả cách 2
| Dòng output | Nội dung |
|---|---|
| 1 | Cách 2: Bảng đối chuẩn kết quả giữa hai đợt chạy: |
| 2 | Dot_chay Tong_mau Loi_schema Ty_le_schema Acc_hop_le Acc_toan_bo So_mau_thuan Xu_ly_ID_10 |
| 3 | Run 1 (Mô phỏng gốc) 10 2 80.0% 62.5% 50.0% 2 Sai |
| 4 | Run 2 (Cải tiến lời nhắc) 10 0 100.0% 100.0% 100.0% 0 Chính xác |
So sánh cách dùng
Cách 1 mở các phép so sánh trên hai đợt phản hồi mô phỏng để thấy rõ từng mẫu số. Cách 2 đóng gói cùng tiêu chí vào hàm báo cáo. Chênh lệch trong ví dụ chưa chứng minh một lời nhắc mới tốt hơn trên dữ liệu thực tế.
Tài liệu tham khảo
- Inside Airbnb. Snapshot dữ liệu thị trường chỗ ở thành phố Santiago ngày 29/06/2026.
- Pydantic Documentation. Data validation and settings management using Python type annotations.
- Google DeepMind. Gemini API structured outputs with JSON Schema documentation.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 7: Data Cleaning and Preparation.
Tài liệu chính thức pandas: Working with missing data & Categorical data.
John W. Tukey, Exploratory Data Analysis, Addison-Wesley, 1977.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
RegexOne, các chủ đề luyện biểu thức chính quy. Bộ đề và ca thử IAI.
IAI. Notebook thực hành và học liệu thực hành 2627-1. Các yêu cầu kỹ thuật được đối chiếu, biên tập và bổ sung lời giải cùng dữ liệu minh họa. Phần thích nghi từ học liệu IAI áp dụng giấy phép CC BY-NC-SA 4.0.