
Làm sạch dữ liệu với pandas: thiếu, trùng, sai kiểu
AIDev TeamTham vấn chuyên môn: Nguyễn Việt Chung
Đăng 12 phút đọc1 lượt xem
Mục lục · 21
Làm sạch dữ liệu bằng pandas gồm bốn việc chính: bỏ dòng trùng, xử lý ô trống, xóa cột thừa và đổi cột về đúng kiểu. Bài làm từng bước trên một file 6.180 đơn hàng bẩn, in số dòng trước và sau mỗi bước, rồi đối chiếu với bản sạch chuẩn: lệch 0 dòng.
Làm sạch dữ liệu bằng pandas là đưa một bảng xuất từ hệ thống về trạng thái tính toán được: mỗi đơn chỉ còn một dòng, ô trống được điền hoặc bỏ có lý do, cột thừa bị xóa và mỗi cột mang đúng kiểu số, chữ hay ngày. Mỗi bước nên in ra số dòng hoặc số ô vừa đổi, để người đọc code biết dữ liệu đã bị can thiệp ở đâu và bao nhiêu. Trên file 6.180 dòng trong bài, sau sáu bước làm sạch còn lại 6.000 đơn, khớp từng ô với bản sạch chuẩn của bộ dữ liệu.
Mấy điều cần nắm trước khi đọc tiếp:
Dữ liệu là file
don_hang_tho.csvcủa cửa hàng mô phỏng "Tiệm Mây", để công khai ở lab.aidev.edu.vn/k2. File có lỗi cài sẵn giống dữ liệu thật: dòng gửi hai lần, đơn có bản cũ, giá viết ba kiểu, ngày không tồn tại.Gọi
df.dropna()không tham số trên file này xóa mất 4.349 dòng, còn 1.831 trên 6.180.pandas 3 bật Copy-on-Write (cơ chế coi mọi bảng con lấy ra từ một bảng là bản sao riêng), nên kiểu viết
df["cot"].fillna(0, inplace=True)trong nhiều tutorial cũ không còn tác dụng.Mọi lệnh trong bài chạy trên pandas 3.1.0, máy ảo AIDev Cloud, ngày 26/10/2026.
Làm sạch dữ liệu gồm những bước nào?
Mình làm theo thứ tự dưới đây. Thứ tự này có lý do: bỏ dòng trùng trước thì các bước đếm ô trống, tính trung vị (giá trị đứng giữa khi xếp từ nhỏ đến lớn) phía sau mới không tính một đơn hai lần.
Bước | Lệnh pandas chính | Trên file Tiệm Mây |
|---|---|---|
1. Bỏ dòng trùng hoàn toàn |
| 120 dòng, còn 6.060 |
2. Giữ bản mới nhất của mỗi đơn |
| 60 bản cũ, còn 6.000 |
3. Xóa cột không dùng |
| bỏ |
4. Xử lý ô trống |
| 4.162 ô mã giảm giá, 180 ô phí vận chuyển |
5. Đổi kiểu dữ liệu |
| giá, mã sản phẩm, ngày |
6. Kiểm tra giá trị lạ | so với bảng giá niêm yết | 1 đơn giá gõ thừa ba số 0 |
Nếu bạn chưa quen DataFrame, Series hay cách đọc file CSV, bài pandas là gì giải thích các khái niệm đó bằng chính bộ dữ liệu này.
Xem dữ liệu bẩn trước khi sửa
Trước khi sửa gì, mình in kiểu của từng cột và đếm ô trống. Script đọc thẳng từ đường link nên bạn chạy được ngay:
import pandas as pd
df = pd.read_csv("https://lab.aidev.edu.vn/k2/du-lieu/don_hang_tho.csv")
print("Ban đầu:", df.shape)
print(df.dtypes)
print("Ô trống mỗi cột:\n", df.isna().sum()[df.isna().sum() > 0])

Hai dấu hiệu đáng ngờ hiện ngay trong dtypes. Cột don_gia mang kiểu str dù tên là đơn giá, nghĩa là có ô chứa chữ. Cột phi_van_chuyen mang kiểu float64 (số thực) dù phí vận chuyển luôn là số nguyên đồng: pandas đổi cả cột sang số thực vì cột có ô trống, mà mảng NumPy chỉ lưu được ô trống NaN (giá trị "không có số") trong kiểu số thực.
Khi bảng nhiều cột, print(df.head()) sẽ ẩn bớt các cột ở giữa bằng dấu .... Muốn xem đủ, đặt tùy chọn hiển thị trước khi in:
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 200)
print(df.head(4))
ma_don ngay_dat cap_nhat_luc ma_kh ma_sp danh_muc so_luong don_gia kenh trang_thai ma_giam_gia phi_van_chuyen
0 DH000001 01/01/2025 2025-01-02 13:20:31 KH1081 SP0080 Mẹ và bé 1 619.000đ Website Hoàn tất NaN NaN
1 DH000002 1/1/2025 2025-01-04 17:26:58 KH0656 SP0082 Mẹ và bé 1 929.000đ Website Hoàn tất TIEMMAY10 20000.0
2 DH000003 01/01/2025 2025-01-01 08:46:57 KH0793 SP0118 Thể thao 1 2.989.000 đ Website Hủy NaN 30000.0
3 DH000004 01/01/2025 2025-01-02 10:31:59 KH0939 SP0085 Mẹ và bé 4 1.129.000đ App Hoàn tất NaN 30000.0
Chỉ ba dòng đầu đã thấy đủ ba kiểu lỗi: ngày viết 01/01/2025 lẫn 1/1/2025, giá viết 619.000đ lẫn 2.989.000 đ, và ô trống ở hai cột cuối.
Xóa dòng trùng trong pandas thế nào?
Dòng trùng có hai loại và phải xử lý riêng. Loại thứ nhất là dòng giống hệt nhau ở mọi cột, thường do hệ thống gửi một đơn hai lần. df.duplicated() đánh dấu các dòng lặp lại (giữ lần xuất hiện đầu), drop_duplicates() xóa chúng:
print("Dòng trùng hoàn toàn:", df.duplicated().sum())
df = df.drop_duplicates()
File có 120 dòng như vậy, bảng còn 6.060 dòng. Loại thứ hai khó thấy hơn: cùng một mã đơn nhưng các cột khác nhau. Dòng lặp loại này là bản xuất cũ, lúc đơn còn "Đang giao", nằm cạnh bản cập nhật sau đó. Mình xếp theo thời điểm cập nhật rồi chỉ giữ dòng cuối của mỗi mã đơn:
print("Mã đơn lặp:", df["ma_don"].duplicated().sum())
df = df.sort_values("cap_nhat_luc").drop_duplicates("ma_don", keep="last").sort_values("ma_don")

Đơn DH000019 có hai dòng: bản lúc 06:26 ngày 02/01/2025 còn "Đang giao", bản lúc 21:10 ngày 06/01/2025 đã "Hoàn tất". Nếu chỉ dùng drop_duplicates() như loại thứ nhất, cả hai dòng đều ở lại vì chúng khác nhau ở cột trạng thái. Báo cáo khi đó sẽ đếm đơn này hai lần, một lần là đơn đang giao. Sau bước này bảng còn đúng 6.000 đơn, mỗi mã đơn một dòng.
Drop columns trong pandas: xóa cột thừa
Cột cap_nhat_luc chỉ cần cho bước 2, giữ bản mới nhất của mỗi đơn; báo cáo không dùng tới. Lệnh xóa cột là drop với tham số columns:
df = df.drop(columns=["cap_nhat_luc"])
Muốn xóa nhiều cột thì đưa cả danh sách, ví dụ df.drop(columns=["cot_a", "cot_b"]). Theo tài liệu DataFrame.drop, viết df.drop("cap_nhat_luc", axis=1) cũng xóa được cột. Mình dùng columns= vì người đọc code nhìn vào là biết ngay lệnh này xóa cột. Phần "Lỗi mình gặp" ở cuối bài cho thấy chuyện gì xảy ra khi quên cả hai cách viết này.
Nếu chỉ cần giữ vài cột trong một bảng rộng, cách ngược lại thường gọn hơn: chọn đúng các cột cần giữ, như df = df[["ma_don", "ngay_dat", "don_gia"]].
Ô trống nên xóa hay điền?
Câu trả lời phụ thuộc vào lý do ô bị trống, nên mình đếm ô trống theo từng nhóm trước khi quyết định. Với cột mã giảm giá:
print(df[df["ma_giam_gia"].isna()]["kenh"].value_counts())

Toàn bộ 1.494 đơn của kênh Sàn TMĐT đều trống mã giảm giá. Ô trống ở đây mang nghĩa "đơn không dùng mã": trong file, đơn của kênh Sàn TMĐT không bao giờ có mã giảm giá của cửa hàng. Xóa mọi dòng trống mã giảm giá là xóa mất 4.162 đơn thật, nên mình điền một giá trị rõ nghĩa:
df["ma_giam_gia"] = df["ma_giam_gia"].fillna("KHONG")
trung_vi = df["phi_van_chuyen"].median()
df["phi_van_chuyen"] = df["phi_van_chuyen"].fillna(trung_vi).astype("int64")
Cột phí vận chuyển thì khác: chỉ có 180 ô trống, khoảng 3% số đơn, giống dữ liệu bị sót khi xuất file hơn là có ý nghĩa riêng. Mình điền bằng trung vị 20.000 đồng rồi đổi cột về số nguyên. So với trung bình, trung vị ít bị vài đơn phí cao kéo lệch. Đây vẫn là một giả định, nên trong báo cáo thật bạn nên ghi rõ có 180 đơn được điền phí.
Đổi kiểu dữ liệu: giá, mã sản phẩm, ngày
Cột giá có ba kiểu viết: 769000, 559.000đ và 349.000 đ. Mình bỏ chữ "đ", dấu chấm và khoảng trắng bằng một biểu thức chính quy (regex, mẫu tìm chuỗi) rồi mới đổi sang số:
df["don_gia"] = df["don_gia"].str.replace(r"[đ\s.]", "", regex=True).astype("int64")
df["ma_sp"] = df["ma_sp"].str.strip().str.upper()
Dòng thứ hai sửa 390 mã sản phẩm bị viết thường hoặc dính khoảng trắng, như sp0080, hoặc SP0080 có dấu cách thừa ở đầu. Nếu không sửa, groupby("ma_sp") (lệnh gom nhóm để cộng, đếm theo từng mã) sẽ coi SP0080 và sp0080 là hai sản phẩm khác nhau.
Cột ngày có ba kiểu viết và 12 ngày không tồn tại như 31/06/2025. Mình đọc bằng hai format cố định: %Y-%m-%d cho kiểu 2025-01-02, %d/%m/%Y cho cả 01/01/2025 lẫn 1/1/2025. Dòng nào không khớp thì để trống (errors="coerce"), rồi ghép hai kết quả lại:
iso = pd.to_datetime(df["ngay_dat"], format="%Y-%m-%d", errors="coerce")
vn = pd.to_datetime(df["ngay_dat"], format="%d/%m/%Y", errors="coerce")
ngay = iso.fillna(vn)
loi = df.loc[ngay.isna(), "ngay_dat"]
sua = pd.to_datetime(loi.str.replace(r"^31/", "30/", regex=True), format="%d/%m/%Y")
df["ngay_dat"] = ngay.fillna(sua)
Mình giả định 12 ngày 31 đó là ngày cuối của tháng có 30 ngày bị gõ nhầm, nên đổi thành ngày 30. Bài dự án Data Analyst mẫu giải thích vì sao không dùng format="mixed" cho cột ngày này: tùy chọn đó âm thầm đảo ngày và tháng của chuỗi kiểu 2025-07-08.
Bước cuối là so đơn giá với bảng giá niêm yết trong san_pham.csv. Đơn DH001651 có đơn giá 1.879.000.000 đồng trong khi giá niêm yết là 1.879.000 đồng, tức gõ thừa ba số 0. Mình ghép giá niêm yết vào từng đơn theo ma_sp bằng merge, sửa đơn giá gấp đúng 1.000 lần giá niêm yết về giá niêm yết, rồi thêm cột doanh_thu bằng số lượng nhân đơn giá:
sp = pd.read_csv("https://lab.aidev.edu.vn/k2/du-lieu/san_pham.csv")[["ma_sp", "gia_niem_yet"]]
df = df.merge(sp, on="ma_sp", how="left")
go_nham = df["don_gia"] == df["gia_niem_yet"] * 1000
df.loc[go_nham, "don_gia"] = df.loc[go_nham, "gia_niem_yet"]
df = df.drop(columns="gia_niem_yet")
df["doanh_thu"] = df["so_luong"] * df["don_gia"]
Kết quả sau khi làm sạch có đúng không?
Bộ dữ liệu Tiệm Mây có sẵn bản sạch don_hang_sach.csv, nên mình đối chiếu từng cột:

Chỉ số | Trước | Sau |
|---|---|---|
Số dòng | 6.180 | 6.000 |
Ô trống | 4.477 | 0 |
Cột |
|
|
Cột |
|
|
Cột |
|
|
Lệch so với bản sạch chuẩn | 0 dòng ở cả 12 cột |
Với dữ liệu công ty thật, bạn sẽ không có bản sạch để so. Khi đó vẫn nên tự viết vài phép kiểm: số đơn trước và sau từng bước, tổng doanh thu theo trạng thái, số mã đơn khác nhau phải bằng số dòng.
Lỗi mình gặp khi làm sạch dữ liệu
dropna() xóa mất 70% bảng
Lệnh hay gặp nhất trong các tutorial là df.dropna(). Trên file này, lệnh đó xóa mọi dòng có ít nhất một ô trống ở bất kỳ cột nào:
6180 -> 1831
6180 -> dropna(subset=['phi_van_chuyen']) 5995

Bảng còn 1.831 dòng vì mọi đơn không dùng mã giảm giá đều bị xóa. Nếu thật sự cần bỏ dòng thiếu, hãy chỉ định cột bằng subset=. Với cột phí vận chuyển, cách viết này chỉ bỏ 185 dòng.
drop thiếu columns: KeyError
Mình gõ vội df.drop("cap_nhat_luc") và nhận:
KeyError: "['cap_nhat_luc'] not found in axis"
Mặc định drop tìm trong index (nhãn của các dòng) chứ không tìm trong tên cột. Không có dòng nào mang nhãn cap_nhat_luc nên pandas báo không thấy. Cách sửa là viết df.drop(columns="cap_nhat_luc").
fillna inplace không có tác dụng trên pandas 3
Kiểu viết df["phi_van_chuyen"].fillna(0, inplace=True) xuất hiện trong rất nhiều tutorial cũ. Trên pandas 3.1.0, lệnh này chỉ in cảnh báo và cột vẫn còn nguyên 185 ô trống:
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.

pandas 3 bật Copy-on-Write: df["phi_van_chuyen"] hành xử như một bản sao, điền vào bản sao không đổi bảng gốc. Cách viết đúng là gán lại, như trong bài: df["cot"] = df["cot"].fillna(gia_tri). Hướng dẫn chuyển sang pandas 3 liệt kê các kiểu viết khác cũng bị ảnh hưởng.
Đổi kiểu khi cột còn ô trống hoặc còn chữ
Gọi astype("int64") trên cột phí vận chuyển khi còn ô trống thì pandas dừng với lỗi IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer. Gọi trên cột đơn giá chưa bỏ chữ "đ" thì gặp ValueError: invalid literal for int() with base 10: '619.000đ'. Hai lỗi này báo đúng chỗ cần sửa, nên mình thấy chúng dễ chịu hơn cách "sửa nhanh" bằng pd.to_numeric(..., errors="coerce"). Cách đó không báo lỗi gì, nhưng biến 4.362 trên 6.180 đơn giá thành ô trống vì mọi giá có chữ "đ" đều không đọc được.
Khi nào không nên tự làm sạch bằng pandas?
Lỗi nằm ở hệ thống nguồn và vẫn đang sinh ra mỗi ngày, ví dụ phần mềm bán hàng cho gõ giá tự do. Sửa ở nguồn rẻ hơn viết lại bước làm sạch mỗi tháng.
Bạn không biết ý nghĩa của ô trống hay giá trị lạ. Giả định như đổi ngày 31 thành ngày 30 chỉ nên làm khi đã hỏi người quản lý dữ liệu.
Dữ liệu lớn hơn RAM của máy. Khi đó hãy làm sạch bằng SQL ngay trong cơ sở dữ liệu.
Câu hỏi thường gặp
Làm sao xóa nhiều cột cùng lúc trong pandas?
Đưa danh sách tên cột vào columns: df = df.drop(columns=["cot_a", "cot_b"]). Nếu có thể có cột không tồn tại, thêm errors="ignore" để pandas bỏ qua thay vì báo KeyError.
Làm sao in đủ mọi cột của DataFrame?
Đặt pd.set_option("display.max_columns", None) trước khi in. Muốn mỗi dòng không bị ngắt xuống, đặt thêm display.width lớn hơn bề ngang bảng, như ví dụ ở đầu bài.
drop_duplicates giữ dòng nào?
Mặc định keep="first" giữ lần xuất hiện đầu tiên. keep="last" giữ lần cuối, keep=False xóa hết mọi dòng bị lặp. Muốn giữ bản mới nhất theo thời gian, bạn phải sort_values theo cột thời gian trước, như bước 2 của bài.
Có nên điền ô trống bằng trung bình không?
Với cột tiền như phí vận chuyển hay doanh thu, trung vị thường an toàn hơn vì vài giá trị rất lớn kéo trung bình lên cao. Với cột chữ, hãy điền một giá trị rõ nghĩa như KHONG thay vì để trống, để groupby vẫn đếm được nhóm đó.
Đọc tiếp
NumPy là gì: mảng số cùng kiểu mà pandas dùng để lưu mỗi cột số, và cách
dtypequyết định phép tính.Python cho Excel: sau khi dữ liệu đã sạch, gộp nhiều file và xuất báo cáo Excel có định dạng.
Data Analyst cần học gì: làm sạch dữ liệu nằm ở đâu trong công việc hằng ngày của Data Analyst.
Chương "Làm sạch dữ liệu" của ebook Phân tích dữ liệu với Python dùng chính file don_hang_tho.csv này, có bài tập tự chấm để bạn luyện từng bước. Nếu muốn học theo đề cương có project được chấm, khóa Phân tích dữ liệu với Python là chặng thứ hai của lộ trình AI Engineer từ số 0.
Code chạy ngày 26/10/2026 trên máy ảo AIDev Cloud: Ubuntu 24.04.4, Intel Xeon E5-2696 v4 25 vCPU, 54 GiB RAM, Python 3.12.3, pandas 3.1.0, NumPy 2.5.4. Dữ liệu Tiệm Mây là dữ liệu mô phỏng của AIDev (giấy phép MIT).
Chuyên mục
Phân tích dữ liệuTham vấn chuyên môn
Nhà sáng lập AIDev và VietPlatform · Thạc sĩ AI MSE
- Tốt nghiệp thủ khoa xuất sắc Thạc sĩ AI MSE.
- Đồng sáng lập, CTO E-University, SVOnline: số hóa 10+ đại học, 40.000+ sinh viên.
- Xây hệ thống AI cho doanh nghiệp tại VietPlatform.


