Bỏ qua đến nội dung chính
Phân tích dữ liệu
Logo pandas phía trên cửa sổ code nối với biểu đồ cột

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.csv củ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

duplicated, drop_duplicates

120 dòng, còn 6.060

2. Giữ bản mới nhất của mỗi đơn

sort_values + drop_duplicates(keep="last")

60 bản cũ, còn 6.000

3. Xóa cột không dùng

drop(columns=...)

bỏ cap_nhat_luc

4. Xử lý ô trống

isna, fillna

4.162 ô mã giảm giá, 180 ô phí vận chuyển

5. Đổi kiểu dữ liệu

str.replace, astype, to_datetime

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:

python
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])
Terminal chạy python lam_sach.py: bảng ban đầu 6180 dòng 12 cột, cột don_gia kiểu str, cột phi_van_chuyen kiểu float64, ô trống ma_giam_gia 4292 và phi_van_chuyen 185, dòng trùng hoàn toàn 120, sau drop_duplicates còn 6060

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:

python
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 200)
print(df.head(4))
text
     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:

python
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:

python
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")
Terminal in mã đơn lặp 60, ví dụ DH000019 có bản Đang giao lúc 2025-01-02 06:26 và bản Hoàn tất lúc 2025-01-06 21:10, DH000022 có bản Đang giao và bản Hủy; sau khi giữ bản mới nhất còn 6000 dòng; danh sách 11 cột còn lại sau khi bỏ cap_nhat_luc

Đơ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:

python
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á:

python
print(df[df["ma_giam_gia"].isna()]["kenh"].value_counts())
Terminal in số đơn trống mã giảm giá theo kênh: Sàn TMĐT 1494, Website 1070, App 865, Facebook 733; phí vận chuyển trống 180, trung vị 20000; ví dụ đơn giá 349.000 đ, 559.000đ, 769000; mã sản phẩm đã chuẩn hóa 390; 12 ngày không đọc được dạng 31/06/2025; đơn DH001651 đơn giá 1.879.000.000 so với giá niêm yết 1.879.000

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:

python
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ố:

python
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:

python
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á:

python
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:

Terminal in kết quả 6000 dòng 12 cột, ngay_dat kiểu datetime64, don_gia, phi_van_chuyen và doanh_thu kiểu int64, ô trống còn lại 0, số đơn 6000 so với 6000, cả 12 cột lệch 0 dòng

Chỉ số

Trước

Sau

Số dòng

6.180

6.000

Ô trống

4.477

0

Cột don_gia

str

int64

Cột phi_van_chuyen

float64

int64

Cột ngay_dat

str

datetime64

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:

text
6180 -> 1831
6180 -> dropna(subset=['phi_van_chuyen']) 5995
Terminal chạy python -u loi.py với pandas 3.1.0: dropna không tham số làm 6180 dòng còn 1831, dropna subset phi_van_chuyen còn 5995; drop thiếu columns báo KeyError cap_nhat_luc not found in axis

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:

text
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:

text
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Terminal chạy loi.py phần tiếp: fillna inplace báo ChainedAssignmentError, ô trống trước 185 sau 185; astype int64 khi còn ô trống báo IntCastingNaNError; astype đơn giá báo ValueError invalid literal for int() with base 10: '619.000đ'; to_numeric errors coerce làm 4362 trên 6180 ô thành NaN

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 dtype quyế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).

Tham vấn chuyên môn

Nguyễn Việt Chung

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.
Biên soạn: AIDev Team · Rà soát · Quy trình biên tập