Mục lục
- Mục tiêu bài học
- Vì sao cần lọc dữ liệu
- Boolean Series — nền tảng của filter
- Toán tử logic
&,|,~ isin,between,isna,notna- Lọc theo chuỗi với
str.contains df.query()— SQL-like- Gán giá trị với
.locvà mask - SettingWithCopyWarning
- View hay copy?
- Use case AI / ML
- Code Python tổng hợp
- Bài tập
- Tóm tắt
Mục tiêu bài học
- Hiểu boolean mask: so sánh trên Series trả về Series
bool, dùng làm chỉ mục cho DataFrame. - Kết hợp nhiều điều kiện đúng cú pháp với
&,|,~và dấu ngoặc. - Dùng
isin,between,isna,notna,str.containsđể viết filter gọn. - Biết dùng
df.query(), gán giá trị qua.loc[mask, col], tránhSettingWithCopyWarning.
Vì sao cần lọc dữ liệu
DataFrame thực tế thường vài chục cột, vài triệu dòng. Phần lớn việc EDA và chuẩn bị data cho ML là chọn subset theo điều kiện:
- Lấy sample của 1 class (ví dụ class
1) để train binary classifier. - Lọc outlier theo
z-scorehoặcIQRtrước khi fit model tuyến tính. - Giữ lại record có timestamp trong khoảng thử nghiệm A/B.
- Báo cáo riêng nhóm khách VIP, theo thành phố, theo trạng thái.
Pandas dùng boolean mask làm công cụ chính cho việc này. Mask là một Series bool cùng độ dài DataFrame, mỗi vị trí trả lời "row này có giữ lại không".
Boolean Series — nền tảng của filter
Phép so sánh trên Series trả về Series bool theo từng phần tử:
import pandas as pd
df = pd.DataFrame({
"name": ["An", "Binh", "Cuong", "Dung", "Hoa"],
"age": [17, 22, 35, 19, 41],
"salary": [0, 1200, 2500, 800, 3200],
})
mask = df["age"] > 18
print(mask)
# 0 False
# 1 True
# 2 True
# 3 True
# 4 True
# Name: age, dtype: bool
Đưa mask vào dấu ngoặc vuông của DataFrame để giữ lại các row mà mask = True:
adults = df[df["age"] > 18]
print(adults)
# name age salary
# 1 Binh 22 1200
# 2 Cuong 35 2500
# 3 Dung 19 800
# 4 Hoa 41 3200
Mask phải cùng độ dài và cùng index với DataFrame. Pandas align theo index, không phải theo vị trí — đây cũng là nguyên nhân hay gặp lỗi khi truyền mask sinh từ một DataFrame khác.
Toán tử logic &, |, ~
Kết hợp nhiều điều kiện dùng toán tử bitwise chứ không phải từ khoá Python:
&— and theo phần tử.|— or theo phần tử.~— not, đảo bit.
Bắt buộc bọc mỗi điều kiện trong dấu ngoặc vì & / | có precedence cao hơn >, <, ==:
import pandas as pd
df = pd.DataFrame({
"name": ["An", "Binh", "Cuong", "Dung", "Hoa"],
"age": [17, 22, 35, 19, 41],
"salary": [0, 1200, 2500, 800, 3200],
})
# Đúng: mỗi điều kiện trong cặp ngoặc riêng
mask = (df["age"] > 18) & (df["salary"] > 1000)
print(df[mask])
# Sai cú pháp — TypeError vì Python parse thành df["age"] > (18 & df["salary"]) > 1000
# df[df["age"] > 18 & df["salary"] > 1000]
Không dùng and, or, not trên Series — pandas sẽ raise ValueError: The truth value of a Series is ambiguous. Lý do: and/or của Python ép Series về 1 giá trị bool duy nhất, nhưng Series có nhiều phần tử nên không xác định được.
# Lỗi điển hình
# df[df["age"] > 18 and df["salary"] > 1000]
# ValueError: The truth value of a Series is ambiguous.
# Use a.empty, a.bool(), a.item(), a.any() or a.all().
# Đảo điều kiện với ~
not_adult = df[~(df["age"] > 18)] # tương đương df["age"] <= 18
isin, between, isna, notna
Pandas có sẵn các phương thức trả Series bool, đọc dễ hơn chuỗi | dài:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"name": ["An", "Binh", "Cuong", "Dung", "Hoa"],
"age": [17, 22, 35, 19, 41],
"city": ["HN", "HCM", "DN", "HN", "HP"],
"email": ["[email protected]", None, "[email protected]", np.nan, "[email protected]"],
})
# isin: thuộc tập giá trị cho trước
df_big_city = df[df["city"].isin(["HN", "HCM"])]
# between: nằm trong khoảng (mặc định inclusive="both")
df_young = df[df["age"].between(20, 30)] # 20 <= age <= 30
df_strict = df[df["age"].between(20, 30, inclusive="neither")] # 20 < age < 30
# isna / notna: kiểm tra NaN
df_missing_email = df[df["email"].isna()]
df_has_email = df[df["email"].notna()]
Kết hợp được với & / | như mask thường:
mask = df["city"].isin(["HN", "HCM"]) & df["age"].between(20, 40)
print(df[mask])
Đảo bằng ~: df[~df["city"].isin(["HN", "HCM"])] — các thành phố khác HN / HCM.
Lọc theo chuỗi với str.contains
Cột object/string có accessor .str với họ phương thức để filter chuỗi:
import pandas as pd
df = pd.DataFrame({
"name": ["Nguyen Van An", "Tran Thi Binh", "Nguyen Hoa", None, "Le Cuong"],
"email": ["[email protected]", "[email protected]", "[email protected]",
"[email protected]", "[email protected]"],
})
# Chứa chuỗi con
df[df["name"].str.contains("Nguyen", na=False)]
# Bắt đầu / kết thúc
df[df["email"].str.endswith("@gmail.com")]
df[df["name"].str.startswith("Le", na=False)]
Tham số na=False rất hay quên: nếu cột có giá trị NaN, str.contains trả NaN cho dòng đó và Pandas báo lỗi khi dùng làm chỉ mục. Truyền na=False để coi NaN như "không match".
str.contains mặc định bật regex=True. Truyền pattern regex để filter mạnh hơn:
import pandas as pd
s = pd.Series(["abc123", "no-digit", "42 items", None])
# Chứa ít nhất 1 chữ số
s[s.str.contains(r"\d", regex=True, na=False)]
# Bắt đầu bằng số
s[s.str.contains(r"^\d+", regex=True, na=False)]
Muốn match literal có ký tự đặc biệt (., ?, (...), tắt regex: str.contains("a.b", regex=False) hoặc dùng re.escape.
df.query() — SQL-like
df.query(expr) nhận điều kiện dạng chuỗi, viết gần như SQL. Khác với mask, trong query dùng được and / or / not hoặc & / | / ~.
import pandas as pd
df = pd.DataFrame({
"name": ["An", "Binh", "Cuong", "Dung", "Hoa"],
"age": [17, 22, 35, 19, 41],
"salary": [0, 1200, 2500, 800, 3200],
"city": ["HN", "HCM", "DN", "HN", "HP"],
})
# Cú pháp giống SQL
df.query("age > 18 and salary > 1000")
# isin
df.query("city in ['HN', 'HCM']")
# Tham chiếu biến Python bằng @
threshold = 18
df.query("age > @threshold")
So với boolean mask:
- Ưu: ngắn, dễ đọc, không phải lặp lại
df["..."]. Tốt cho điều kiện nhiều cột. - Nhược: column name có dấu cách / ký tự đặc biệt phải bọc backtick (
"`my col` > 0"); không kiểm tra cú pháp lúc viết code, lỗi chỉ phát sinh runtime; chậm hơn mask một chút trên DataFrame rất nhỏ.
Quy tắc thực dụng: filter có 1–2 điều kiện đơn giản → boolean mask. Filter dài, nhiều phép so sánh, đọc giống SQL → query.
Gán giá trị với .loc và mask
Lọc thường không dừng ở việc đọc — còn dùng để gán giá trị có điều kiện. Cú pháp chuẩn là df.loc[mask, "col"] = value:
import pandas as pd
df = pd.DataFrame({
"name": ["An", "Binh", "Cuong", "Dung"],
"age": [17, 22, 35, 19],
})
# Thêm cột category dựa trên điều kiện
df["category"] = "minor"
df.loc[df["age"] >= 18, "category"] = "adult"
print(df)
# name age category
# 0 An 17 minor
# 1 Binh 22 adult
# 2 Cuong 35 adult
# 3 Dung 19 adult
Cũng có thể lấy nhiều cột cùng một mask:
subset = df.loc[df["age"] >= 18, ["name", "category"]]
Nhiều người mới hay viết df[df["age"] >= 18]["category"] = "adult". Cách này thường không update df và sinh ra SettingWithCopyWarning — xem mục tiếp theo.
SettingWithCopyWarning
Warning này phát sinh khi gán giá trị trên kết quả của filter "chained":
import pandas as pd
df = pd.DataFrame({"age": [17, 22, 35], "score": [5, 7, 9]})
sub = df[df["age"] > 18] # sub có thể là view hoặc copy — không guarantee
sub["score"] = 10 # SettingWithCopyWarning
Pandas không chắc sub là view của df hay là DataFrame mới, nên không rõ phép gán có tác dụng lên df hay không. Hai cách fix rõ ràng:
# Cách 1: gán thẳng trên df gốc bằng .loc
df.loc[df["age"] > 18, "score"] = 10
# Cách 2: nếu cần một subset độc lập, gọi .copy() rồi mới sửa
sub = df[df["age"] > 18].copy()
sub["score"] = 10 # không còn warning, df gốc không đổi
Lưu ý phiên bản: Pandas 3.x sẽ bật Copy-on-Write mặc định (pd.options.mode.copy_on_write = True). Khi đó sub luôn là copy, hành vi tường minh hơn — nhưng quy tắc dùng .loc để gán trên df gốc vẫn là cách viết khuyến nghị.
View hay copy?
Một câu hỏi hay được hỏi: "Khi lọc df[mask], kết quả là view hay copy của df gốc?"
Câu trả lời chính xác: không guarantee. Pandas có thể trả view, có thể trả copy, tuỳ dtype và layout bộ nhớ. Vì vậy đừng dựa vào việc sửa kết quả filter sẽ (hay không) ảnh hưởng df gốc.
Quy tắc thực dụng:
- Muốn đọc subset →
df[mask]là đủ. - Muốn sửa df gốc theo điều kiện → dùng
df.loc[mask, col] = .... - Muốn DataFrame mới độc lập để biến đổi →
df[mask].copy().
Trên Pandas 3.x bật Copy-on-Write, mọi filter mặc định trả "copy logic": sửa kết quả không bao giờ ảnh hưởng df gốc, và không còn SettingWithCopyWarning nữa.
Use case AI / ML
- Stratified sampling: lấy
nsample mỗi class —df[df["label"] == c].sample(n)cho từng class rồipd.concat. - Lọc outlier theo z-score:
mask = (df["x"] - df["x"].mean()).abs() / df["x"].std() < 3giữ lại điểm trong 3 sigma. - Lọc outlier theo IQR: tính
q1,q3, giữdf[df["x"].between(q1 - 1.5 * iqr, q3 + 1.5 * iqr)]. - Cắt window thời gian:
df[df["ts"].between("2025-01-01", "2025-03-31")]để chỉ giữ Q1. - Lọc text theo từ khoá:
df[df["text"].str.contains(r"\b(?:gpu|cuda)\b", regex=True, case=False, na=False)]trước khi đưa qua tokenizer / embedding. - Lọc dòng không có label:
df[df["label"].notna()]trước khitrain_test_split.
Code Python tổng hợp
DataFrame sinh viên với nhiều cột; lần lượt dùng mask, isin, between, str.contains và query:
import numpy as np
import pandas as pd
df = pd.DataFrame({
"id": [1, 2, 3, 4, 5, 6, 7, 8],
"name": ["Nguyen An", "Tran Binh", "Le Cuong", "Pham Dung",
"Nguyen Hoa", "Do Khanh", "Nguyen Lan", "Vu Minh"],
"age": [18, 22, 35, 19, 41, 28, 17, 30],
"score": [8.5, 7.0, 9.2, 6.5, 8.0, np.nan, 7.8, 9.0],
"city": ["HN", "HCM", "DN", "HN", "HP", "HCM", "HN", "DN"],
})
# 1) Boolean mask đa điều kiện — nhớ dấu ngoặc
mask = (df["age"].between(20, 35)) & (df["score"] >= 8.0)
print("Adult điểm cao:")
print(df[mask])
# 2) isin: chỉ giữ 3 thành phố
big_city = df[df["city"].isin(["HN", "HCM", "DN"])]
print("3 thành phố lớn:", big_city.shape)
# 3) str.contains: tên bắt đầu bằng "Nguyen"
nguyen = df[df["name"].str.contains(r"^Nguyen", regex=True, na=False)]
print(nguyen[["id", "name"]])
# 4) Lọc missing label trước khi train
ready = df[df["score"].notna()].copy()
# 5) query — tương đương mask ở (1)
q = df.query("20 <= age <= 35 and score >= 8.0")
print("Bằng query:", q.shape)
# 6) Gán cột phân loại với .loc — tránh SettingWithCopyWarning
df["group"] = "khac"
df.loc[df["score"] >= 8.5, "group"] = "gioi"
df.loc[df["score"].between(7.0, 8.5, inclusive="left"), "group"] = "kha"
print(df[["name", "score", "group"]])
Bài tập
- Tạo DataFrame
employees(id, name, age, salary, city). Lọc nhân viên cósalary > 1000vàagetrong khoảng[25, 40]. Yêu cầu dùng đúng&và dấu ngoặc, không dùngand. - Dùng
isinđể chỉ giữ các dòng cócitythuộc["HN", "HCM", "DN"]. Sau đó dùng~để in ra các dòng không thuộc 3 thành phố này. - Dùng
str.contains(r"^Nguyen", regex=True, na=False)để lọc các tên bắt đầu bằngNguyen. Thayregex=Truebằngstr.startswithvà so sánh kết quả. - Viết cùng một bộ lọc ở bài 1 bằng
df.query(); so sánh độ dài, kiểm tra(mask_df.values == query_df.values).all(). - (Mở rộng) Lọc outlier trên cột
salarytheo z-score: giữ lại các dòng có|z| < 3. So sánh shape trước và sau khi lọc.
Tóm tắt
- Boolean mask = Series
boolcùng index với DataFrame;df[mask]giữ lại row mà mask True. - Kết hợp điều kiện dùng
&,|,~và bọc mỗi điều kiện trong dấu ngoặc; không dùngand/or/nottrên Series. - Phương thức tiện:
isin,between,isna,notna; chuỗi dùng.str.containsvớina=Falsevà tham sốregex. df.query("expr")SQL-like, đọc gọn cho điều kiện dài; tham chiếu biến với@var.- Gán giá trị có điều kiện qua
df.loc[mask, col] = ...; tránhSettingWithCopyWarningbằng.lochoặc.copy(). - Filter không guarantee view hay copy — đọc thì OK, ghi thì luôn dùng
.lochoặc.copy().
