Học kì 1, năm học 2026–2027
Viện Trí tuệ nhân tạo, Trường ĐH Công nghệ, ĐHQGHN
Tên chỗ ở, giá tiền và danh sách tiện nghi.
Ví dụ thật dùng bảng df: chỗ ở tại Santiago, snapshot 29/06/2026. Giá: CLP/đêm.
Bỏ khoảng trắng thừa, đổi về chữ thường và lọc theo từ khoá.
Với một chuỗi Python, viết " Ñuñoa ".strip().lower().
s = pd.Series([" Ñuñoa ", "ÑUÑOA", "ñuñoa", "Nunoa"])
chuan = s.str.strip().str.lower()
print(chuan.tolist())
['ñuñoa', 'ñuñoa', 'ñuñoa', 'nunoa']
Series không có strip; .str áp phương thức chuỗi cho từng giá trị, trả Series mới cùng chỉ mục; giá trị thiếu vẫn là thiếu.
strip bỏ khoảng trắng ở hai đầu; lower đổi sang chữ thường.
print(s.value_counts())
Ñuñoa 1
ÑUÑOA 1
ñuñoa 1
Nunoa 1
Name: count, dtype: int64
print(chuan.value_counts())
ñuñoa 3
nunoa 1
Name: count, dtype: int64
nunoa vẫn khác ñuñoa. Sửa chính tả cần quy tắc riêng.
Lấy những tên có nhắc đến metro.
ten = pd.Series(["Metro centro", "Casa con jardín"])
mask = ten.str.contains("metro", case=False, regex=False)
print(ten[mask].tolist())
['Metro centro']
case=False: không phân biệt hoa thường.regex=False: tìm nguyên chuỗi đã cho.
Regex sẽ được giải thích ở mục 2.
s_thieu = pd.Series(["metro", None, "centro"], dtype="string")
kq = pd.DataFrame({
"khong_na": s_thieu.str.contains("metro"),
"na_False": s_thieu.str.contains("metro", na=False),
})
print(kq)
khong_na na_False
0 True True
1 <NA> False
2 False False
Tên thiếu cho <NA> với dtype="string", cho False với kiểu str mặc định.
na=False loại tên thiếu khỏi kết quả lọc, nhưng không chứng minh chỗ ở ấy không có metro.
Trong nguồn này, $ là ký hiệu giá bằng CLP; dấu phẩy phân cách hàng nghìn.
gia_chu = (df["price"]
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False))
df["price_num"] = pd.to_numeric(gia_chu, errors="coerce")
print(df[["price", "price_num"]].head(3))
price price_num
0 $45,647.00 45647.0
1 $19,856.00 19856.0
2 $46,776.00 46776.0
errors="coerce" chuyển chuỗi không hợp lệ thành NaN (giá trị thiếu), thay vì báo lỗi.
Ví dụ: "hỏi chủ" → NaN.
def bo_ky_hieu(x):
if isinstance(x, str):
return x.replace("$", "").replace(",", "")
return x
gia_chu = df["price"].apply(bo_ky_hieu)
| Lưu chuỗi | .str.replace × 2 | Hàm + apply |
|---|---|---|
PyArrow | 2,7 | 13,0 |
object | 13,1 | 11,1 |
PyArrow là thư viện Python của Apache Arrow: lưu theo cột, xử lý bằng C++. Khi đã cài PyArrow, pandas 3 dùng nó để lưu chuỗi.
apply gọi hàm Python cho từng giá trị. Với object, .str cũng xử lý từng chuỗi Python. Hãy kiểm kết quả hai cách rồi đo.
loi_doc = df["price"].notna() & df["price_num"].isna()
print(pd.Series({
"thieu_tu_nguon": df["price"].isna().sum(),
"khong_doc_duoc": loi_doc.sum(),
}))
thieu_tu_nguon 846
khong_doc_duoc 0
dtype: int64
Giữ giá gốc để phân biệt giá thiếu từ nguồn với lỗi chuyển đổi.
Với ba giá ["$45,647.00", None, "hỏi chủ"], phép kiểm này cho 1 giá thiếu từ nguồn và 1 lỗi chuyển đổi.
Dữ liệu giả lập, USD/đêm:
| Loại | Giá |
|---|---|
" Entire home " | 300 |
"entire HOME" | thiếu |
"PRIVATE ROOM" | 40 |
"Private room " | 60 |
"private room" | 200 |
Tính giá trung vị theo loại, gộp các nhãn chỉ khác hoa thường hoặc khoảng trắng.
Lấy số phòng, diện tích ghi trong tên chỗ ở.
Biểu thức chính quy (regular expression, gọi tắt là regex) mô tả mẫu ký tự cần tìm trong chuỗi.
s_cham = pd.Series(["a.b", "axb"])
print(pd.DataFrame({
"regex": s_cham.str.contains("."),
"chuoi": s_cham.str.contains(".", regex=False),
}))
regex chuoi
0 True True
1 True False
Trong regex, . khớp một ký tự bất kỳ,
trừ ký tự xuống dòng theo mặc định.
contains mặc định coi mẫu là regex; đặt regex=False để tìm nguyên chuỗi.
| Tên (giả lập) | Phần cần lấy |
|---|---|
| Modern 1BR Oasis | 1 |
| Casa 3 bedrooms | 3 |
| Studio 2 br centro | 2 |
| Torre 2024 | không có |
Mẫu cần tìm: chữ số → khoảng trắng tuỳ ý → BR hoặc bedroom(s).
r"(\d+)\s*(?:BR|bedrooms?)"
| Phần | Ý nghĩa |
|---|---|
\d+ | một hoặc nhiều chữ số |
\s* | không có hoặc có nhiều ký tự trắng |
BR|bedrooms? | BR, bedroom hoặc bedrooms |
(\d+) | Nhóm: extract trả về các chữ số này,3 bedrooms → "3" |
(?:BR|bedrooms?) | Ngoặc giới hạn phạm vi của |;?: để extract không trả về phần này. |
Tiền tố r (raw string) giữ nguyên \ trong \d, \s, \b.
ten_mau = pd.Series(["Modern 1BR Oasis", "Casa 3 bedrooms",
"12bravo centro", "Torre 2024"])
pat = r"(\d+)\s*(?:BR|bedrooms?)"
print(ten_mau.str.extract(pat, flags=re.I, expand=False))
0 1
1 3
2 12
3 NaN
dtype: str
extract lấy nhóm trong lần khớp đầu tiên.
flags=re.I: không phân biệt hoa thường, nên br trong 12bravo cũng khớp BR. Mẫu còn quá rộng.
PAT_PHONG = r"\b(\d+)\s*(?:BR|bedrooms?)\b"
trich = ten_mau.str.extract(
PAT_PHONG, flags=re.I, expand=False)
print(trich)
0 1
1 3
2 NaN
3 NaN
dtype: str
\b là ranh giới từ: vị trí một bên là chữ hoặc số, bên kia không phải (hoặc đầu/cuối chuỗi). Sau BR trong bravo vẫn là chữ, nên không khớp.
so_phong = pd.to_numeric(trich, errors="coerce")
print(so_phong.tolist())
[1.0, 3.0, nan, nan]
Giá trị thiếu nghĩa là quy tắc này chưa lấy được số, không phải số phòng bằng 0.
Mã hợp lệ gồm chữ P và các chữ số. "P12x" và "mã P12" có được nhận là mã không?
ma = pd.Series(["P12", "P12x", "mã P12"])
print(pd.DataFrame({
"co_doan_khop": ma.str.contains(r"P\d+", na=False),
"khop_ca_chuoi": ma.str.fullmatch(r"P\d+", na=False),
}))
co_doan_khop khop_ca_chuoi
0 True True
1 True False
2 True False
Có đoạn khớp chưa chứng tỏ cả chuỗi đúng định dạng.
| Tên (giả lập) | Kết quả mong muốn (m²) |
|---|---|
| Depto 45 m2 | 45 |
| Studio 30m² | 30 |
| 80 M2 luxury | 80 |
| Torre 2024 | thiếu |
| 30m2x | thiếu |
Viết mẫu trích diện tích để cho đúng kết quả trong bảng.
trich_that = df["name"].str.extract(
PAT_PHONG, flags=re.I, expand=False)
print(pd.Series({
"so_ten": len(df),
"khop_mau": trich_that.notna().sum(),
}))
so_ten 18534
khop_mau 339
dtype: int64
flags=re.I: không phân biệt hoa thường.expand=False: mẫu chỉ có một nhóm nên trả về Series.expand=True: trả về DataFrame.Mẫu chỉ khớp các cách viết BR, bedroom(s). Cần xem cả tên khớp và tên không khớp.
| Tên (giả lập) | Có chữ metro? |
|---|---|
| Apartment near metro | True |
| No metro nearby | True |
Nên đặt tên cột nhac_metro. Muốn kết luận gần metro, cần dữ liệu khác, như khoảng cách đến ga.
Tách danh sách tiện nghi để đếm và tính tỷ lệ.
nhan = pd.Series(["Wifi;Kitchen", "Kitchen;Parking"],
index=["P1", "P2"])
print(nhan.str.split(";", regex=False))
P1 [Wifi, Kitchen]
P2 [Kitchen, Parking]
dtype: object
Kết quả là một danh sách trong mỗi ô. Dấu ; là quy ước của ví dụ này.
raw = pd.Series(['["Wifi", "Kitchen"]',
'["Kitchen", "Stove, gas"]', '[]'],
index=["P1", "P2", "P3"])
ds = raw.map(json.loads)
print(ds)
P1 [Wifi, Kitchen]
P2 [Kitchen, Stove, gas]
P3 []
dtype: object
print(ds["P2"])
['Kitchen', 'Stove, gas']
P2 có hai tiện nghi; Stove, gas là một giá trị. Cắt chuỗi JSON theo dấu phẩy sẽ tách nó làm đôi.
explode() đưa mỗi phần tử trong danh sách thành một hàng.P3 có danh sách rỗng thì còn trong kết quả không?
tien_nghi = ds.explode()
print(tien_nghi)
P1 Wifi
P1 Kitchen
P2 Kitchen
P2 Stove, gas
P3 NaN
dtype: str
Chỉ mục vẫn là mã chỗ ở. Danh sách rỗng [] của P3 tạo một hàng NaN.
dropna() bỏ các hàng có giá trị thiếu trong Series.
tien_nghi = tien_nghi.dropna()
print(tien_nghi)
P1 Wifi
P1 Kitchen
P2 Kitchen
P2 Stove, gas
dtype: str
Hàng P3: NaN bị bỏ. Dữ liệu gốc ds vẫn có đủ ba chỗ ở.
Ví dụ có 3 chỗ ở và 4 mục tiện nghi.
co_wifi = ds.map(lambda x: "Wifi" in x)
print(f"{co_wifi.sum()}/{len(ds)} chỗ ở có Wifi "
f"({co_wifi.mean():.1%})")
1/3 chỗ ở có Wifi (33.3%)
la_wifi = tien_nghi.eq("Wifi")
print(f"{la_wifi.sum()}/{len(tien_nghi)} mục tiện nghi là Wifi "
f"({la_wifi.mean():.1%})")
1/4 mục tiện nghi là Wifi (25.0%)
Mẫu số là 3 chỗ ở (kể cả P3 có danh sách rỗng), không phải 4 mục tiện nghi: 33,3% khác 25,0%.
Gộp nhãn chỉ khác hoa thường hoặc khoảng trắng ở hai đầu.
| Chỗ ở | Danh sách tiện nghi (giả lập) |
|---|---|
| P1 | ["Wifi", " wifi ", "Kitchen"] |
| P2 | ["WIFI", "Kitchen"] |
| P3 | ["Kitchen", "Parking", "Parking"] |
| P4 | [] |
Lập bảng cho biết mỗi tiện nghi có bao nhiêu chỗ ở liệt kê nó.
ds_that = df["amenities"].map(json.loads)
co_wifi_that = ds_that.map(lambda x: "Wifi" in x)
print(f"{co_wifi_that.sum():,}/{len(df):,} chỗ ở "
f"({co_wifi_that.mean():.1%}) liệt kê Wifi")
16,772/18,534 chỗ ở (90.5%) liệt kê Wifi
| Cần làm | Cách chọn |
|---|---|
| Gộp các cách viết | strip, lower và quy tắc gộp rõ ràng |
| Tìm nguyên chuỗi | contains(..., regex=False) |
| Trích / kiểm cả chuỗi | extract / fullmatch |
| Tách nhiều giá trị | split hoặc json.loads → explode |
Giữ dữ liệu gốc, đọc cả tên khớp lẫn tên không khớp, và tính theo đúng đơn vị quan sát (chỗ ở hay mục tiện nghi).