zalo-icon
facebook-icon
phone-icon
Làm sạch dữ liệu bằng Python: Từ dữ liệu thô đến phân tích | Data Cleaning with Python

Dữ liệu doanh nghiệp hiếm khi sẵn sàng để phân tích ngay sau khi được trích xuất. Một file có thể chứa tên cột không thống nhất, giá trị thiếu, bản ghi trùng, ngày tháng sai định dạng, số tiền được lưu dưới dạng chuỗi và nhiều quy tắc nghiệp vụ bị vi phạm. Bài viết này xây dựng một project Python hoàn chỉnh để biến dữ liệu khách hàng thô thành dataset sạch, có thể kiểm tra và sẵn sàng cho phân tích.

Mục lục

  1. Bài toán thực tế
  2. Sau bài này bạn sẽ làm được gì?
  3. Kiến thức Python cần dùng
  4. Chuẩn bị môi trường
  5. Hiểu dữ liệu đầu vào
  6. Xây dựng ứng dụng từng bước
  7. Ghép thành chương trình hoàn chỉnh
  8. Kiểm tra kết quả
  9. Những lỗi thường gặp
  10. Nâng cấp từ demo thành ứng dụng thực tế
  11. Bài tập thực hành
  12. Project mở rộng
  13. Tổng kết
  14. Tài liệu tham khảo

1. Bài toán thực tế

Một doanh nghiệp bán lẻ đang chuẩn bị phân tích hành vi khách hàng. Bộ phận kinh doanh cung cấp file customers_raw.csv gồm dữ liệu được tổng hợp từ website, cửa hàng và chương trình thành viên. File có khoảng 200.000 dòng nhưng không thể đưa thẳng vào dashboard. Cùng một khách hàng có thể xuất hiện nhiều lần với cách viết tên khác nhau. Số điện thoại có dòng bắt đầu bằng số 0, có dòng chứa mã quốc gia và có dòng kèm khoảng trắng. Email đôi khi viết hoa, sai cấu trúc hoặc bị bỏ trống. Ngày sinh được nhập theo nhiều định dạng, còn tổng chi tiêu có thể chứa dấu phân cách hàng nghìn và ký hiệu tiền tệ.

Nếu người phân tích bỏ qua những vấn đề này, số lượng khách hàng có thể bị tính cao hơn thực tế, doanh thu theo phân khúc bị sai và chiến dịch chăm sóc có thể gửi nhiều thông báo cho cùng một người. Nguy hiểm hơn, dashboard vẫn hiển thị những con số có vẻ hợp lý nên lỗi chỉ được phát hiện sau khi quyết định kinh doanh đã được đưa ra.

Project trong bài sẽ xây dựng một data cleaning pipeline có thể đọc dữ liệu thô, chuẩn hóa tên cột, xử lý giá trị thiếu, chuyển đổi kiểu dữ liệu, chuẩn hóa email và số điện thoại, phát hiện duplicate, kiểm tra business rule và xuất riêng dữ liệu hợp lệ cùng dữ liệu cần xem xét. Mỗi thay đổi đều được đo lường để người vận hành biết pipeline đã tác động đến dữ liệu như thế nào.

2. Sau bài này bạn sẽ làm được gì?

Sau khi hoàn thành, bạn sẽ có một chương trình nhận file data/raw/customers_raw.csv và tạo ba sản phẩm trong thư mục output. File customers_clean.csv chứa dữ liệu hợp lệ đã chuẩn hóa. File customers_invalid.csv giữ các bản ghi cần kiểm tra kèm nguyên nhân. File quality_report.csv ghi số lượng đầu vào, duplicate, bản ghi lỗi, bản ghi hợp lệ và tỷ lệ dữ liệu đạt yêu cầu.

Quan trọng hơn, bạn sẽ thực hành tư duy làm sạch dữ liệu theo quy tắc có thể giải thích. Chương trình không tự ý điền mọi giá trị thiếu hoặc xóa tất cả outlier. Mỗi quyết định đều dựa trên ý nghĩa nghiệp vụ, mức độ tin cậy của dữ liệu và khả năng truy vết về nguồn.

3. Kiến thức Python cần dùng

Project sử dụng Pandas để đọc, kiểm tra và biến đổi dữ liệu. Các kỹ thuật chính gồm làm việc với DataFrame, chuẩn hóa chuỗi bằng accessor .str, chuyển đổi ngày bằng pd.to_datetime(), chuyển đổi số bằng pd.to_numeric(), xử lý missing value, tạo Boolean mask và dùng duplicated() để xác định bản ghi trùng.

Thư viện re trong Python được dùng để loại ký tự không cần thiết khỏi số điện thoại. Module pathlib quản lý đường dẫn đầu vào và đầu ra. Code được chia thành function để từng quy tắc có thể được kiểm thử độc lập.

4. Chuẩn bị môi trường

Project nên chạy với Python 3.11 trở lên trong một virtual environment riêng. Cài Pandas bằng các command sau:

python -m venv .venv
source .venv/bin/activate
python -m pip install pandas

Trên Windows, command kích hoạt môi trường là:

.venvScriptsactivate

Cấu trúc project:

python-data-cleaning/
    data/
        raw/
            customers_raw.csv
    output/
    main.py
    requirements.txt

5. Hiểu dữ liệu đầu vào

File mẫu có các cột customer_id, full_name, email, phone, date_of_birth, city, registered_attotal_spend. Trước khi làm sạch, cần phân biệt ba nhóm vấn đề: lỗi kỹ thuật, lỗi định dạng và lỗi nghiệp vụ.

CộtÝ nghĩaKiểu mong đợiQuy tắc chính
customer_idMã khách hàngStringKhông trống và duy nhất
full_nameHọ tênStringKhông trống
emailEmail liên hệStringChuẩn hóa chữ thường và kiểm tra cấu trúc
phoneSố điện thoạiStringChỉ giữ chữ số và chuẩn hóa đầu số
date_of_birthNgày sinhDateKhông nằm trong tương lai
registered_atNgày đăng kýDatetimePhải là ngày hợp lệ
total_spendTổng chi tiêuNumericKhông âm

Không phải cột nào thiếu cũng có cách xử lý giống nhau. Thiếu customer_id khiến bản ghi không thể nhận diện nên nên được đưa vào vùng lỗi. Thiếu email có thể vẫn chấp nhận nếu doanh nghiệp liên hệ bằng số điện thoại. Thiếu thành phố có thể gán nhãn unknown nếu báo cáo cần một nhóm rõ ràng. Việc điền giá trị phải dựa trên mục đích sử dụng, không dựa trên thói quen kỹ thuật.

6. Xây dựng ứng dụng từng bước

Bước 1: Đọc dữ liệu và kiểm tra schema

Vấn đề. Pipeline không nên tiếp tục nếu file thiếu các cột bắt buộc. Việc kiểm tra sớm giúp lỗi được phát hiện tại điểm gần nguồn nhất.

from pathlib import Path
import re

import pandas as pd


INPUT_FILE = Path("data/raw/customers_raw.csv")
OUTPUT_DIR = Path("output")

REQUIRED_COLUMNS = [
    "customer_id",
    "full_name",
    "email",
    "phone",
    "date_of_birth",
    "city",
    "registered_at",
    "total_spend",
]


def extract(file_path: Path) -> pd.DataFrame:
    frame = pd.read_csv(
        file_path,
        dtype={
            "customer_id": "string",
            "phone": "string",
        },
        encoding="utf-8",
    )

    frame.columns = [
        str(column).strip().lower()
        for column in frame.columns
    ]

    missing_columns = sorted(
        set(REQUIRED_COLUMNS) - set(frame.columns)
    )

    if missing_columns:
        raise ValueError(
            f"Thiếu cột bắt buộc: {missing_columns}"
        )

    return frame[REQUIRED_COLUMNS].copy()

Giải thích code. Mã khách hàng và số điện thoại được đọc dưới dạng string vì đây là định danh, không phải đại lượng dùng để tính toán. Sau khi chuẩn hóa tên cột, chương trình so sánh schema thực tế với danh sách bắt buộc. File thiếu cột sẽ bị từ chối thay vì tạo kết quả không đầy đủ.

Bước 2: Chuẩn hóa dữ liệu text

Vấn đề. Khoảng trắng thừa và cách viết hoa không nhất quán làm tăng số lượng giá trị phân biệt. Thành phố Hồ Chí Minh, hồ chí minh HỒ CHÍ MINH có thể bị tính thành ba nhóm.

def normalize_text(
    frame: pd.DataFrame,
) -> pd.DataFrame:
    cleaned = frame.copy()

    text_columns = [
        "customer_id",
        "full_name",
        "email",
        "phone",
        "city",
    ]

    for column in text_columns:
        cleaned[column] = (
            cleaned[column]
            .astype("string")
            .str.strip()
            .replace("", pd.NA)
        )

    cleaned["full_name"] = (
        cleaned["full_name"]
        .str.replace(r"s+", " ", regex=True)
        .str.title()
    )

    cleaned["email"] = (
        cleaned["email"]
        .str.lower()
    )

    cleaned["city"] = (
        cleaned["city"]
        .str.replace(r"s+", " ", regex=True)
        .str.title()
        .fillna("Unknown")
    )

    return cleaned

Giải thích code. Khoảng trắng ở hai đầu được loại bỏ, nhiều khoảng trắng liên tiếp trong tên và thành phố được thu gọn. Email được chuyển về chữ thường vì phần lớn hệ thống không phân biệt chữ hoa và chữ thường khi nhận diện địa chỉ. Việc dùng title() cho tên chỉ phù hợp với dữ liệu mẫu; tên thật có thể chứa quy tắc viết hoa đặc biệt nên production cần bảng ngoại lệ hoặc không thay đổi cách viết gốc.

Bước 3: Chuẩn hóa số điện thoại

Vấn đề. Số điện thoại có thể chứa khoảng trắng, dấu chấm, dấu ngoặc hoặc mã quốc gia. Cần đưa về một quy ước thống nhất trước khi dùng để đối chiếu khách hàng.

def normalize_phone(
    value: object,
) -> object:
    if pd.isna(value):
        return pd.NA

    digits = re.sub(
        r"D",
        "",
        str(value),
    )

    if digits.startswith("84"):
        digits = "0" + digits[2:]

    if len(digits) != 10:
        return pd.NA

    return digits

Giải thích code. Regular expression loại mọi ký tự không phải chữ số. Số bắt đầu bằng mã quốc gia 84 được đổi về định dạng trong nước. Project mẫu yêu cầu số điện thoại có 10 chữ số. Trong hệ thống thật, cần kiểm tra đầu số hợp lệ, quốc gia của khách hàng và khả năng một số điện thoại được nhiều người cùng sử dụng.

Bước 4: Chuyển đổi ngày và dữ liệu số

Vấn đề. Ngày và số được lưu dưới dạng text không thể dùng an toàn cho phép tính. Giá trị không chuyển đổi được cần được đánh dấu thay vì làm pipeline dừng ngay lập tức.

def convert_types(
    frame: pd.DataFrame,
) -> pd.DataFrame:
    cleaned = frame.copy()

    cleaned["date_of_birth"] = pd.to_datetime(
        cleaned["date_of_birth"],
        errors="coerce",
        dayfirst=True,
    )

    cleaned["registered_at"] = pd.to_datetime(
        cleaned["registered_at"],
        errors="coerce",
        dayfirst=True,
    )

    spend_text = (
        cleaned["total_spend"]
        .astype("string")
        .str.replace(",", "", regex=False)
        .str.replace("VND", "", regex=False)
        .str.strip()
    )

    cleaned["total_spend"] = pd.to_numeric(
        spend_text,
        errors="coerce",
    )

    cleaned["phone"] = (
        cleaned["phone"]
        .apply(normalize_phone)
        .astype("string")
    )

    return cleaned

Giải thích code. Giá trị ngày không hợp lệ được chuyển thành NaT. Tổng chi tiêu được loại dấu phân cách và chuỗi tiền tệ trước khi chuyển sang số. Quy tắc này dựa trên dữ liệu mẫu dùng dấu phẩy để phân cách hàng nghìn. Nếu dữ liệu có nhiều locale, việc thay dấu phẩy một cách máy móc có thể làm sai phần thập phân.

Bước 5: Phát hiện duplicate

Vấn đề. Duplicate không đơn giản là hai dòng giống hoàn toàn. Hai bản ghi có thể đại diện cho cùng một khách hàng nhưng khác cách viết tên hoặc thiếu một số trường.

def remove_duplicates(
    frame: pd.DataFrame,
) -> tuple[pd.DataFrame, int]:
    sorted_frame = frame.sort_values(
        "registered_at",
        na_position="first",
    )

    duplicate_mask = sorted_frame.duplicated(
        subset=["customer_id"],
        keep="last",
    )

    duplicate_count = int(
        duplicate_mask.sum()
    )

    result = sorted_frame.loc[
        ~duplicate_mask
    ].copy()

    return result, duplicate_count

Giải thích code. Project xem customer_id là khóa chuẩn và giữ bản ghi có thời gian đăng ký mới nhất. Đây chỉ là quy tắc minh họa. Nếu hệ thống nguồn tái sử dụng mã, hoặc một khách hàng có nhiều mã, doanh nghiệp cần Entity Resolution hoặc Master Data Management thay vì chỉ dùng drop_duplicates().

Bước 6: Kiểm tra business rule

Vấn đề. Kiểu dữ liệu đúng không đồng nghĩa dữ liệu hợp lệ. Ngày sinh nằm trong tương lai vẫn là datetime, còn tổng chi tiêu âm vẫn là numeric.

def validate(
    frame: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    checked = frame.copy()
    today = pd.Timestamp.today().normalize()

    email_pattern = (
        r"^[A-Za-z0-9._%+-]+"
        r"@[A-Za-z0-9.-]+"
        r".[A-Za-z]{2,}$"
    )

    conditions = {
        "missing_customer_id": (
            checked["customer_id"].isna()
        ),
        "missing_full_name": (
            checked["full_name"].isna()
        ),
        "invalid_email": (
            checked["email"].notna()
            & ~checked["email"].str.match(
                email_pattern,
                na=False,
            )
        ),
        "invalid_phone": (
            checked["phone"].isna()
        ),
        "invalid_date_of_birth": (
            checked["date_of_birth"].notna()
            & (
                checked["date_of_birth"] > today
            )
        ),
        "invalid_registered_at": (
            checked["registered_at"].isna()
        ),
        "invalid_total_spend": (
            checked["total_spend"].isna()
            | (checked["total_spend"] < 0)
        ),
    }

    checked["validation_errors"] = ""

    for error_name, mask in conditions.items():
        checked.loc[
            mask,
            "validation_errors",
        ] += error_name + ";"

    valid_mask = checked[
        "validation_errors"
    ].eq("")

    valid_rows = checked.loc[
        valid_mask
    ].copy()

    invalid_rows = checked.loc[
        ~valid_mask
    ].copy()

    valid_rows = valid_rows.drop(
        columns="validation_errors"
    )

    return valid_rows, invalid_rows

Giải thích code. Mỗi quy tắc tạo một Boolean mask. Một bản ghi có thể có nhiều lỗi nên các mã lỗi được nối trong cùng cột. Email chỉ bị xem là sai khi có giá trị nhưng không khớp cấu trúc cơ bản. Regex không chứng minh hộp thư tồn tại; nó chỉ phát hiện các lỗi định dạng rõ ràng.

7. Ghép thành chương trình hoàn chỉnh

from pathlib import Path
import re

import pandas as pd


INPUT_FILE = Path(
    "data/raw/customers_raw.csv"
)
OUTPUT_DIR = Path("output")

REQUIRED_COLUMNS = [
    "customer_id",
    "full_name",
    "email",
    "phone",
    "date_of_birth",
    "city",
    "registered_at",
    "total_spend",
]


def extract(
    file_path: Path,
) -> pd.DataFrame:
    frame = pd.read_csv(
        file_path,
        dtype={
            "customer_id": "string",
            "phone": "string",
        },
        encoding="utf-8",
    )

    frame.columns = [
        str(column).strip().lower()
        for column in frame.columns
    ]

    missing_columns = sorted(
        set(REQUIRED_COLUMNS)
        - set(frame.columns)
    )

    if missing_columns:
        raise ValueError(
            "Thiếu cột bắt buộc: "
            f"{missing_columns}"
        )

    return frame[
        REQUIRED_COLUMNS
    ].copy()


def normalize_phone(
    value: object,
) -> object:
    if pd.isna(value):
        return pd.NA

    digits = re.sub(
        r"D",
        "",
        str(value),
    )

    if digits.startswith("84"):
        digits = "0" + digits[2:]

    if len(digits) != 10:
        return pd.NA

    return digits


def transform(
    frame: pd.DataFrame,
) -> pd.DataFrame:
    cleaned = frame.copy()

    text_columns = [
        "customer_id",
        "full_name",
        "email",
        "phone",
        "city",
    ]

    for column in text_columns:
        cleaned[column] = (
            cleaned[column]
            .astype("string")
            .str.strip()
            .replace("", pd.NA)
        )

    cleaned["full_name"] = (
        cleaned["full_name"]
        .str.replace(
            r"s+",
            " ",
            regex=True,
        )
        .str.title()
    )

    cleaned["email"] = (
        cleaned["email"]
        .str.lower()
    )

    cleaned["city"] = (
        cleaned["city"]
        .str.replace(
            r"s+",
            " ",
            regex=True,
        )
        .str.title()
        .fillna("Unknown")
    )

    cleaned["phone"] = (
        cleaned["phone"]
        .apply(normalize_phone)
        .astype("string")
    )

    cleaned["date_of_birth"] = (
        pd.to_datetime(
            cleaned["date_of_birth"],
            errors="coerce",
            dayfirst=True,
        )
    )

    cleaned["registered_at"] = (
        pd.to_datetime(
            cleaned["registered_at"],
            errors="coerce",
            dayfirst=True,
        )
    )

    spend_text = (
        cleaned["total_spend"]
        .astype("string")
        .str.replace(
            ",",
            "",
            regex=False,
        )
        .str.replace(
            "VND",
            "",
            regex=False,
        )
        .str.strip()
    )

    cleaned["total_spend"] = (
        pd.to_numeric(
            spend_text,
            errors="coerce",
        )
    )

    return cleaned


def remove_duplicates(
    frame: pd.DataFrame,
) -> tuple[pd.DataFrame, int]:
    sorted_frame = frame.sort_values(
        "registered_at",
        na_position="first",
    )

    duplicate_mask = (
        sorted_frame.duplicated(
            subset=["customer_id"],
            keep="last",
        )
    )

    duplicate_count = int(
        duplicate_mask.sum()
    )

    result = sorted_frame.loc[
        ~duplicate_mask
    ].copy()

    return result, duplicate_count


def validate(
    frame: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    checked = frame.copy()
    today = pd.Timestamp.today().normalize()

    email_pattern = (
        r"^[A-Za-z0-9._%+-]+"
        r"@[A-Za-z0-9.-]+"
        r".[A-Za-z]{2,}$"
    )

    conditions = {
        "missing_customer_id": (
            checked["customer_id"].isna()
        ),
        "missing_full_name": (
            checked["full_name"].isna()
        ),
        "invalid_email": (
            checked["email"].notna()
            & ~checked["email"].str.match(
                email_pattern,
                na=False,
            )
        ),
        "invalid_phone": (
            checked["phone"].isna()
        ),
        "invalid_date_of_birth": (
            checked["date_of_birth"].notna()
            & (
                checked["date_of_birth"]
                > today
            )
        ),
        "invalid_registered_at": (
            checked["registered_at"].isna()
        ),
        "invalid_total_spend": (
            checked["total_spend"].isna()
            | (
                checked["total_spend"]
                < 0
            )
        ),
    }

    checked["validation_errors"] = ""

    for error_name, mask in conditions.items():
        checked.loc[
            mask,
            "validation_errors",
        ] += error_name + ";"

    valid_mask = checked[
        "validation_errors"
    ].eq("")

    valid_rows = checked.loc[
        valid_mask
    ].copy()

    invalid_rows = checked.loc[
        ~valid_mask
    ].copy()

    valid_rows = valid_rows.drop(
        columns="validation_errors"
    )

    return valid_rows, invalid_rows


def export_results(
    valid_rows: pd.DataFrame,
    invalid_rows: pd.DataFrame,
    quality_report: pd.DataFrame,
) -> None:
    OUTPUT_DIR.mkdir(
        parents=True,
        exist_ok=True,
    )

    valid_rows.to_csv(
        OUTPUT_DIR / "customers_clean.csv",
        index=False,
        encoding="utf-8-sig",
    )

    invalid_rows.to_csv(
        OUTPUT_DIR / "customers_invalid.csv",
        index=False,
        encoding="utf-8-sig",
    )

    quality_report.to_csv(
        OUTPUT_DIR / "quality_report.csv",
        index=False,
        encoding="utf-8-sig",
    )


def main() -> None:
    raw_data = extract(INPUT_FILE)
    input_count = len(raw_data)

    transformed = transform(raw_data)

    (
        deduplicated,
        duplicate_count,
    ) = remove_duplicates(
        transformed
    )

    (
        valid_rows,
        invalid_rows,
    ) = validate(
        deduplicated
    )

    quality_report = pd.DataFrame(
        [
            ("input_records", input_count),
            (
                "duplicates_removed",
                duplicate_count,
            ),
            (
                "invalid_records",
                len(invalid_rows),
            ),
            (
                "valid_records",
                len(valid_rows),
            ),
            (
                "valid_rate",
                round(
                    len(valid_rows)
                    / input_count,
                    4,
                ),
            ),
        ],
        columns=["metric", "value"],
    )

    export_results(
        valid_rows,
        invalid_rows,
        quality_report,
    )

    print(
        quality_report.to_string(
            index=False
        )
    )


if __name__ == "__main__":
    main()

8. Kiểm tra kết quả

Giả sử file đầu vào có 200.000 bản ghi. Chương trình phát hiện 4.250 duplicate, 3.180 bản ghi không hợp lệ và tạo 192.570 bản ghi sạch. Output có thể giống như sau:

              metric       value
       input_records   200000.00
  duplicates_removed     4250.00
     invalid_records     3180.00
       valid_records   192570.00
          valid_rate        0.96

Phép đối soát số lượng cần xác nhận rằng dữ liệu đầu vào bằng duplicate đã loại cộng với dữ liệu lỗi và dữ liệu hợp lệ. Tiếp theo, kiểm tra phân bố lỗi trong customers_invalid.csv. Nếu phần lớn bản ghi lỗi đến từ một nguyên nhân hoặc một hệ thống nguồn, biện pháp tốt nhất thường là sửa quy trình nhập liệu ở nguồn thay vì tiếp tục bổ sung logic làm sạch.

Nên chọn một mẫu ngẫu nhiên từ dữ liệu hợp lệ để đối chiếu với nguồn. Đồng thời chọn các bản ghi ở biên như khách hàng có chi tiêu bằng 0, ngày sinh thiếu hoặc chỉ có một kênh liên hệ. Mục tiêu là bảo đảm business rule không loại nhầm các trường hợp hợp lệ nhưng ít phổ biến.

9. Những lỗi thường gặp

Dùng fillna cho mọi giá trị thiếu

Điền cùng một giá trị cho mọi cột có thể tạo dữ liệu giả. Thiếu thành phố có thể gán nhóm Unknown, nhưng thiếu ngày sinh không nên tự động điền ngày trung bình nếu dữ liệu được dùng cho chiến dịch theo độ tuổi.

Xóa toàn bộ duplicate mà không xác định khóa

drop_duplicates() trên toàn bộ cột chỉ phát hiện những dòng giống hoàn toàn. Ngược lại, dùng một khóa quá rộng có thể xóa nhầm. Duplicate phải được định nghĩa theo business key và quy tắc chọn bản ghi giữ lại.

Dùng regex email như bằng chứng địa chỉ tồn tại

Regex chỉ kiểm tra cấu trúc. Nó không xác nhận domain nhận email, hộp thư tồn tại hay người dùng đồng ý nhận thông tin. Production cần quản lý trạng thái xác minh và consent riêng.

Ép kiểu và bỏ qua lỗi

errors="coerce" hữu ích để pipeline tiếp tục, nhưng mọi giá trị bị chuyển thành missing phải được thống kê. Nếu chỉ ép kiểu rồi loại dòng null, chương trình có thể che giấu thay đổi dữ liệu nghiêm trọng.

Thay đổi dữ liệu gốc

Không nên ghi đè file raw. Dữ liệu gốc cần được giữ nguyên để truy vết, chạy lại pipeline và điều tra khi quy tắc làm sạch thay đổi.

10. Nâng cấp từ demo thành ứng dụng thực tế

Trong doanh nghiệp, pipeline cần tách dữ liệu thành ít nhất ba vùng: raw giữ nguyên đầu vào, clean chứa dữ liệu đã chuẩn hóa và quarantine chứa bản ghi không đạt quy tắc. Mỗi lần chạy cần có run ID, thời điểm xử lý, phiên bản code, nguồn dữ liệu và số lượng bản ghi ở từng trạng thái.

Quy tắc chất lượng không nên nằm rải rác trong nhiều function mà cần được quản lý như một data contract. Tên cột, kiểu dữ liệu, điều kiện bắt buộc, miền giá trị và ngưỡng lỗi cần có chủ sở hữu. Khi quy tắc thay đổi, phiên bản contract cũng phải thay đổi để kết quả cũ có thể được giải thích.

Với dữ liệu lớn, CSV có thể được thay bằng Parquet để giảm dung lượng và giữ kiểu dữ liệu tốt hơn. Dữ liệu sạch có thể được nạp vào PostgreSQL, BigQuery hoặc Data Warehouse. Công cụ orchestration lập lịch pipeline, còn hệ thống monitoring theo dõi thời gian chạy, tỷ lệ lỗi, schema drift và thay đổi bất thường trong phân bố dữ liệu.

Thông tin khách hàng là dữ liệu nhạy cảm. Production cần mã hóa, kiểm soát truy cập theo vai trò, giới hạn dữ liệu trong log và áp dụng chính sách lưu giữ. File lỗi không được xem là dữ liệu ít quan trọng vì nó vẫn có thể chứa email, số điện thoại và thông tin nhận diện cá nhân.

11. Bài tập thực hành

Level 1: Bổ sung quy tắc tuổi hợp lệ

Tính tuổi từ ngày sinh và đánh dấu dữ liệu có tuổi nhỏ hơn 0 hoặc lớn hơn 120. Không tự động xóa ngày sinh thiếu. Báo cáo riêng số dòng không có ngày sinh và số dòng có ngày sinh không hợp lệ.

Level 2: Chuẩn hóa tỉnh thành

Tạo bảng mapping cho các biến thể như TP HCM, HCMHồ Chí Minh. Những giá trị không có trong mapping phải được đưa vào danh sách cần review thay vì tự động ghép theo phỏng đoán.

Level 3: Tạo báo cáo data quality theo ngày

Lưu kết quả chất lượng của mỗi lần chạy vào một bảng lịch sử. Xây báo cáo theo dõi valid rate, duplicate rate và top lỗi phổ biến. Gửi cảnh báo khi tỷ lệ hợp lệ giảm mạnh so với mức trung bình gần đây.

12. Project mở rộng

Hãy xây một Customer Data Quality Pipeline cho dữ liệu từ website, CRM và cửa hàng. Project cần đọc nhiều nguồn, ánh xạ schema, chuẩn hóa thông tin liên hệ, phát hiện duplicate, giữ lineage về hệ thống nguồn và xuất một customer master table. Mỗi bản ghi cần có trạng thái chất lượng và thời điểm cập nhật gần nhất.

Portfolio nên gồm source code, dữ liệu giả lập, unit test, tài liệu business rule, báo cáo chất lượng và phần giải thích các giới hạn. Không sử dụng dữ liệu khách hàng thật trong repository công khai.

13. Tổng kết

Bạn vừa dùng Python và Pandas để biến một file khách hàng thô thành dataset có thể kiểm tra và sử dụng cho phân tích. Project đã chuẩn hóa text, số điện thoại, ngày tháng và dữ liệu số; phát hiện duplicate; kiểm tra business rule; tách dữ liệu lỗi; đồng thời tạo báo cáo chất lượng.

Kỹ năng quan trọng nhất không phải ghi nhớ các hàm Pandas mà là biết dữ liệu nào có thể sửa tự động, dữ liệu nào cần đưa vào vùng review và cách chứng minh pipeline không làm mất thông tin ngoài dự kiến.

14. Tài liệu tham khảo

  1. Pandas documentation, Working with missing data: https://pandas.pydata.org/docs/user_guide/missing_data.html
  2. Pandas documentation, Working with text data: https://pandas.pydata.org/docs/user_guide/text.html
  3. Pandas documentation, pandas.to_datetime: https://pandas.pydata.org/docs/reference/api/pandas.to_datetime.html
  4. Pandas documentation, pandas.to_numeric: https://pandas.pydata.org/docs/reference/api/pandas.to_numeric.html
  5. Pandas documentation, DataFrame.duplicated: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.duplicated.html
  6. Python documentation, Regular expression operations: https://docs.python.org/3/library/re.html
  7. Python documentation, pathlib: https://docs.python.org/3/library/pathlib.html
Scroll to Top