Python 7 — Pandas & NumPy cho phân tích dữ liệu

13 thg 7, 2026 3 lượt xem
#data-analysis
#python
#numpy
#pandas

Mở đầu: vì sao một nhân viên phân tích dữ liệu ngân hàng cần NumPy và pandas?

Hãy tưởng tượng bạn nhận một file CSV chứa 5 triệu giao dịch trong tháng: mã khách hàng, số tiền, loại giao dịch (chuyển khoản, rút tiền, nạp tiền), ngày giờ, chi nhánh. Sếp hỏi: "Tổng số dư biến động theo từng chi nhánh? Khách hàng nào có giao dịch đáng ngờ trên 500 triệu? Có bao nhiêu dòng bị thiếu thông tin?"

Nếu chỉ dùng Python thuần với vòng lặp for, bạn sẽ viết hàng chục dòng và chương trình chạy hàng phút. Với NumPypandas, mỗi câu hỏi trên chỉ tốn một đến hai dòng và chạy trong vài trăm mili-giây. Đây là lý do hai thư viện này là nền tảng của mọi công việc phân tích dữ liệu, khoa học dữ liệu và machine learning trong Python.

Bài này đi từ con số 0 (mảng là gì) đến mức "hero" (gộp nhóm, nối bảng, pivot, xử lý dữ liệu thực tế) với rất nhiều ví dụ trong bối cảnh ngân hàng.

Cài đặt: pip install numpy pandas. Trong code ta quy ước import numpy as npimport pandas as pd.


NumPy: nền móng của mọi tính toán số

ndarray — mảng N chiều

ndarray (N-dimensional array) là kiểu dữ liệu trung tâm của NumPy. Khác với list của Python, mọi phần tử trong ndarray có cùng một kiểu (thường là số) và được lưu liền nhau trong bộ nhớ. Chính điều này khiến nó nhanh.

import numpy as np

# Tạo mảng từ list
so_du = np.array([1_200_000, 5_400_000, 800_000, 12_000_000])
print(so_du.dtype)   # int64
print(so_du.shape)   # (4,)  -> mảng 1 chiều, 4 phần tử
print(so_du.ndim)    # 1

# Mảng 2 chiều: 3 khách hàng, 4 tháng số dư
ma_tran = np.array([
    [1.2, 1.5, 1.1, 2.0],
    [0.8, 0.9, 1.0, 1.1],
    [5.0, 4.8, 5.2, 6.0],
])
print(ma_tran.shape)  # (3, 4)

# Các cách khởi tạo nhanh
np.zeros((2, 3))          # ma trận 0
np.ones(5)                # [1. 1. 1. 1. 1.]
np.arange(0, 10, 2)       # [0 2 4 6 8]
np.linspace(0, 1, 5)      # 5 điểm cách đều từ 0 đến 1
np.random.seed(42)
np.random.rand(3)         # 3 số ngẫu nhiên [0,1)

Vectorization: vì sao nhanh hơn vòng lặp?

Giả sử cần tính lãi 6%/năm cho 1 triệu số dư. Cách "Python thuần":

so_du = list(range(1_000_000))
lai = []
for x in so_du:
    lai.append(x * 0.06)

Mỗi vòng lặp, Python phải: kiểm tra kiểu của x, gọi hàm nhân, tạo đối tượng mới. Đây là chi phí của ngôn ngữ thông dịch (interpreted).

Cách NumPy — vectorization (tính trên cả mảng cùng lúc):

so_du = np.arange(1_000_000)
lai = so_du * 0.06        # một biểu thức, áp dụng cho TẤT CẢ phần tử

Tại sao nhanh hơn 10–100 lần?

  1. Vòng lặp chạy trong C, không phải trong Python. NumPy gọi xuống mã C đã biên dịch sẵn.
  2. Bộ nhớ liền khối giúp CPU đọc dữ liệu hiệu quả (cache locality).
  3. SIMD — CPU xử lý nhiều số trong một lệnh.
import time

n = 5_000_000
a = np.arange(n)

t0 = time.perf_counter()
tong_loop = sum(x * 2 for x in a)        # chậm
t1 = time.perf_counter()
tong_vec = (a * 2).sum()                 # nhanh
t2 = time.perf_counter()

print(f"Vòng lặp: {t1 - t0:.3f}s")       # thường ~0.5-1s
print(f"Vectorized: {t2 - t1:.3f}s")     # thường ~0.01s

Quy tắc vàng: tránh vòng lặp for trên mảng số; tìm phép toán vector tương đương.

Broadcasting — cộng/nhân mảng khác kích thước

Broadcasting là cơ chế NumPy "kéo giãn" mảng nhỏ để khớp mảng lớn mà không tốn bộ nhớ sao chép.

# Số dư 3 khách qua 4 tháng (triệu đồng)
sd = np.array([
    [1.2, 1.5, 1.1, 2.0],
    [0.8, 0.9, 1.0, 1.1],
    [5.0, 4.8, 5.2, 6.0],
])

# Cộng phí 0.1 triệu cho MỌI ô (số vô hướng "lan" ra toàn mảng)
print(sd + 0.1)

# Trừ số dư trung bình của từng tháng (mảng 4 phần tử lan theo từng hàng)
tb_thang = sd.mean(axis=0)        # shape (4,)
print(sd - tb_thang)              # (3,4) - (4,) -> hợp lệ, lan theo cột

Quy tắc broadcasting: so chiều từ phải sang trái; hai chiều tương thích nếu bằng nhau hoặc một trong hai bằng 1.

Các thao tác thống kê & axis

sd.sum()            # tổng tất cả
sd.sum(axis=0)      # tổng theo cột (gộp các hàng) -> tổng từng tháng
sd.sum(axis=1)      # tổng theo hàng -> tổng cả năm từng khách
sd.mean(), sd.max(), sd.min(), sd.std()
np.where(sd > 2, "cao", "thấp")     # if-else trên cả mảng

Nhớ: axis=0 đi dọc theo các hàng (kết quả là một giá trị cho mỗi cột), axis=1 đi ngang theo các cột.


pandas: làm việc với bảng dữ liệu thật

NumPy mạnh với số thuần. Nhưng dữ liệu ngân hàng có nhãn cột ("so_tien", "chi_nhanh"), có nhiều kiểu (ngày, chuỗi, số), có giá trị thiếu. Đó là lúc cần pandas.

Series và DataFrame

  • Series: mảng 1 chiều có nhãn (index). Giống một cột.
  • DataFrame: bảng 2 chiều — tập hợp nhiều Series chung index. Giống một bảng SQL hoặc sheet Excel.
import pandas as pd

# Series
so_du = pd.Series([1.2, 0.8, 5.0], index=["KH001", "KH002", "KH003"], name="so_du")
print(so_du["KH002"])     # 0.8

# DataFrame từ dict
df = pd.DataFrame({
    "ma_kh":     ["KH001", "KH002", "KH003", "KH004"],
    "chi_nhanh": ["HN", "HN", "HCM", "HCM"],
    "so_tien":   [1_200_000, 800_000, 5_000_000, 300_000],
    "loai_gd":   ["chuyen", "rut", "chuyen", "nap"],
})
print(df.head())          # 5 dòng đầu
print(df.info())          # kiểu, số dòng, bộ nhớ
print(df.describe())      # thống kê cột số
print(df.shape)           # (4, 4)

Đọc dữ liệu: read_csv và read_sql

Trong thực tế bạn hiếm khi gõ tay; bạn đọc từ file hoặc database.

# Đọc CSV
df = pd.read_csv(
    "giao_dich_thang_06.csv",
    sep=",",
    parse_dates=["ngay_gd"],          # tự chuyển sang kiểu ngày
    dtype={"ma_kh": "string"},        # ép kiểu cột
    na_values=["", "NULL", "N/A"],    # coi các chuỗi này là thiếu
)

# Đọc theo từng khối (chunk) khi file quá lớn để vừa RAM
tong = 0
for chunk in pd.read_csv("giao_dich_lon.csv", chunksize=100_000):
    tong += chunk["so_tien"].sum()

Đọc trực tiếp từ database (ví dụ PostgreSQL của hệ thống core banking):

from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@host:5432/corebank")

df = pd.read_sql(
    """
    SELECT ma_kh, chi_nhanh, so_tien, loai_gd, ngay_gd
    FROM giao_dich
    WHERE ngay_gd >= '2026-06-01'
    """,
    con=engine,
    parse_dates=["ngay_gd"],
)

Mẹo: nên lọc bớt ở SQL (WHERE, LIMIT) trước khi đưa về pandas, tránh kéo cả triệu dòng không cần thiết.

Chọn và lọc dữ liệu: loc, iloc, boolean indexing

Đây là kỹ năng dùng nhiều nhất. Phân biệt rõ ba cách:

# 1. Chọn cột
df["so_tien"]                 # một cột -> Series
df[["ma_kh", "so_tien"]]      # nhiều cột -> DataFrame

# 2. .loc — chọn theo NHÃN (tên cột, giá trị index)
df.loc[0, "so_tien"]                       # ô tại index 0, cột so_tien
df.loc[0:2, ["ma_kh", "chi_nhanh"]]        # dải index 0..2 (BAO GỒM 2)

# 3. .iloc — chọn theo VỊ TRÍ số (như mảng)
df.iloc[0, 2]                 # hàng 0, cột thứ 3
df.iloc[0:2, :]               # 2 hàng đầu (KHÔNG bao gồm 2)

# 4. Boolean indexing — lọc theo điều kiện (rất quan trọng)
df[df["so_tien"] > 1_000_000]                       # giao dịch lớn
df[(df["chi_nhanh"] == "HN") & (df["so_tien"] > 500_000)]   # AND dùng &
df[df["loai_gd"].isin(["chuyen", "rut"])]            # thuộc danh sách
df[df["so_tien"].between(100_000, 1_000_000)]        # trong khoảng

# Phát hiện giao dịch đáng ngờ
nghi_van = df[df["so_tien"] >= 500_000_000]

Lưu ý: với điều kiện kết hợp dùng &, |, ~ (không dùng and, or, not) và bọc mỗi vế trong ngoặc.

Xử lý giá trị thiếu (NaN)

Dữ liệu thật luôn có ô trống: khách chưa khai chi nhánh, lỗi nhập liệu. pandas dùng NaN (Not a Number) để biểu diễn.

df.isna().sum()               # đếm số ô thiếu của từng cột
df["chi_nhanh"].isna().any()  # có thiếu không?

# Xóa dòng/cột thiếu
df.dropna()                          # bỏ dòng có BẤT KỲ ô nào thiếu
df.dropna(subset=["so_tien"])        # chỉ bỏ khi thiếu so_tien
df.dropna(axis=1)                    # bỏ cột có thiếu

# Điền giá trị thiếu
df["chi_nhanh"] = df["chi_nhanh"].fillna("KHONG_RO")
df["so_tien"] = df["so_tien"].fillna(df["so_tien"].median())   # điền trung vị
df["so_du"] = df["so_du"].fillna(method="ffill")               # lấy giá trị trước

Nguyên tắc nghiệp vụ: không tùy tiện điền số tiền giao dịch bằng 0 vì làm sai tổng tiền. Hãy hiểu vì sao thiếu rồi mới chọn cách xử lý.

groupby & aggregate — gộp nhóm

Câu hỏi "tổng tiền theo chi nhánh" chính là groupby. Mô hình: split → apply → combine (chia nhóm → tính trên mỗi nhóm → ghép lại).

# Tổng số tiền theo chi nhánh
df.groupby("chi_nhanh")["so_tien"].sum()

# Nhiều thống kê cùng lúc
df.groupby("chi_nhanh")["so_tien"].agg(["sum", "mean", "count", "max"])

# Gộp theo nhiều cột
df.groupby(["chi_nhanh", "loai_gd"])["so_tien"].sum()

# Tên cột kết quả tùy chỉnh (named aggregation)
bao_cao = df.groupby("chi_nhanh").agg(
    tong_tien=("so_tien", "sum"),
    so_giao_dich=("so_tien", "count"),
    tien_lon_nhat=("so_tien", "max"),
).reset_index()
print(bao_cao)

merge / join — nối nhiều bảng

Thông tin thường nằm rải rác: bảng giao_dichma_kh, còn tên và hạng khách hàng ở bảng khach_hang. Nối chúng bằng merge.

khach_hang = pd.DataFrame({
    "ma_kh":  ["KH001", "KH002", "KH003"],
    "ten":    ["An", "Bình", "Cường"],
    "hang":   ["Vàng", "Bạc", "Kim cương"],
})

# inner: chỉ giữ ma_kh có ở CẢ hai bảng
kq = df.merge(khach_hang, on="ma_kh", how="inner")

# left: giữ TẤT CẢ giao dịch, thêm thông tin khách nếu có
kq = df.merge(khach_hang, on="ma_kh", how="left")

Bảng so sánh kiểu join:

howGiữ dòng nàoDùng khi
innerChỉ key có ở cả hai bảngCần dữ liệu khớp hoàn toàn
leftTất cả bảng trái, NaN nếu phải thiếuGiữ toàn bộ giao dịch
rightTất cả bảng phảiHiếm dùng, thường đảo thành left
outerTất cả từ cả hai, NaN ở chỗ thiếuĐối soát, tìm dòng lệch

pivot & pivot_table — xoay bảng thành ma trận

Muốn xem tổng tiền theo (chi nhánh × loại giao dịch) dạng bảng chéo:

bang_cheo = df.pivot_table(
    index="chi_nhanh",       # hàng
    columns="loai_gd",       # cột
    values="so_tien",        # giá trị tổng hợp
    aggfunc="sum",           # cách gộp
    fill_value=0,            # ô trống -> 0
    margins=True,            # thêm dòng/cột "All" (tổng)
)
print(bang_cheo)

pivot_table khác pivot ở chỗ tự xử lý khi có trùng key (nó gộp bằng aggfunc), nên trong phân tích thực tế hầu như luôn dùng pivot_table.

apply / map — áp dụng hàm tùy chỉnh

Khi không có phép vector sẵn, bạn viết hàm riêng.

# map trên Series: ánh xạ giá trị
muc_phi = {"chuyen": 0.0003, "rut": 0.0, "nap": 0.0}
df["phi"] = df["loai_gd"].map(muc_phi) * df["so_tien"]

# apply trên Series
df["so_tien_trieu"] = df["so_tien"].apply(lambda x: round(x / 1_000_000, 2))

# apply trên DataFrame theo từng dòng (axis=1)
def phan_loai(row):
    if row["so_tien"] >= 500_000_000:
        return "Báo cáo NHNN"
    elif row["so_tien"] >= 100_000_000:
        return "Theo dõi"
    return "Bình thường"

df["canh_bao"] = df.apply(phan_loai, axis=1)

Hiệu năng: apply(axis=1) thực chất vẫn lặp từng dòng nên chậm. Ưu tiên vector hóa (np.where, np.select) hoặc map khi có thể. Chỉ dùng apply cho logic phức tạp khó vector hóa.

# Phiên bản vector hóa nhanh của phan_loai
dieu_kien = [df["so_tien"] >= 500_000_000, df["so_tien"] >= 100_000_000]
nhan = ["Báo cáo NHNN", "Theo dõi"]
df["canh_bao"] = np.select(dieu_kien, nhan, default="Bình thường")

Sắp xếp và cột mới

df.sort_values("so_tien", ascending=False)
df.sort_values(["chi_nhanh", "so_tien"], ascending=[True, False])

# Tạo cột mới
df["sau_phi"] = df["so_tien"] - df["phi"]

# Xử lý cột ngày
df["thang"] = df["ngay_gd"].dt.month
df["ngay_trong_tuan"] = df["ngay_gd"].dt.day_name()

Xuất kết quả

bao_cao.to_csv("bao_cao_chi_nhanh.csv", index=False, encoding="utf-8-sig")
bao_cao.to_excel("bao_cao.xlsx", sheet_name="ChiNhanh", index=False)
bao_cao.to_sql("bao_cao_thang", con=engine, if_exists="replace", index=False)
bao_cao.to_parquet("bao_cao.parquet")   # nén tốt, đọc nhanh, giữ kiểu

encoding="utf-8-sig" giúp Excel mở file CSV tiếng Việt không bị lỗi font.


Bảng so sánh nhanh các thao tác

Mục đíchNumPypandas
Đơn vị dữ liệundarraySeries, DataFrame
Có nhãn cột/indexKhông
Đa kiểu trong một bảngKhông (đồng nhất)
Chọn theo nhãn.loc
Chọn theo vị tríarr[i, j].iloc
Lọc điều kiệnarr[arr > 0]df[df["c"] > 0]
Gộp nhómnp.add.reduceatdf.groupby(...).agg(...)
Nối bảngnp.concatenatedf.merge(...)
Giá trị thiếunp.nan, np.isnanNaN, isna/fillna/dropna

Luồng phân tích dữ liệu điển hình


Ví dụ tổng hợp end-to-end

Gắn các mảnh lại thành một quy trình thật: đọc giao dịch, làm sạch, gộp nhóm, nối thông tin khách hàng, xuất báo cáo.

import pandas as pd
import numpy as np

# 1. Đọc
gd = pd.read_csv("giao_dich.csv", parse_dates=["ngay_gd"],
                 na_values=["", "NULL"])

# 2. Làm sạch
gd = gd.dropna(subset=["so_tien", "ma_kh"])
gd["chi_nhanh"] = gd["chi_nhanh"].fillna("KHONG_RO")
gd["so_tien"] = gd["so_tien"].astype("float64")

# 3. Lọc giao dịch hợp lệ trong tháng
gd = gd[(gd["so_tien"] > 0) & (gd["ngay_gd"].dt.month == 6)]

# 4. Cột cảnh báo (vector hóa)
gd["canh_bao"] = np.select(
    [gd["so_tien"] >= 500_000_000, gd["so_tien"] >= 100_000_000],
    ["Báo cáo NHNN", "Theo dõi"],
    default="Bình thường",
)

# 5. Tổng hợp theo chi nhánh
bao_cao = gd.groupby("chi_nhanh").agg(
    tong_tien=("so_tien", "sum"),
    so_gd=("so_tien", "count"),
    gd_lon=("canh_bao", lambda s: (s == "Báo cáo NHNN").sum()),
).reset_index()

# 6. Nối hạng khách của giao dịch lớn nhất mỗi chi nhánh (ví dụ merge)
kh = pd.read_csv("khach_hang.csv")
chi_tiet = gd.merge(kh[["ma_kh", "hang"]], on="ma_kh", how="left")

# 7. Xuất
bao_cao.to_csv("bao_cao_thang_06.csv", index=False, encoding="utf-8-sig")
print(bao_cao)

Tóm tắt

  • NumPy cung cấp ndarray đồng nhất, lưu liền khối; vectorization thay vòng lặp Python bằng vòng lặp C nên nhanh hơn nhiều lần; broadcasting cho phép tính giữa các mảng khác kích thước.
  • pandas dựng trên NumPy, thêm nhãn và đa kiểu: Series (một cột) và DataFrame (bảng).
  • Đọc dữ liệu bằng read_csv / read_sql; lọc bằng .loc, .iloc, boolean indexing; xử lý thiếu bằng dropna / fillna.
  • Tổng hợp bằng groupby().agg(), xoay bảng bằng pivot_table, nối bảng bằng merge với các kiểu inner / left / outer.
  • Ưu tiên thao tác vector (np.where, np.select, map) hơn apply(axis=1) để giữ tốc độ; cuối cùng xuất bằng to_csv / to_sql / to_excel.

Tự kiểm tra

  1. Vì sao tính trên ndarray bằng vectorization nhanh hơn vòng lặp for trên list Python? Nêu ít nhất hai lý do.
  2. Phân biệt .loc.iloc; khi viết df.loc[0:2]df.iloc[0:2] thì số dòng trả về có khác nhau không? Tại sao?
  3. Khi lọc theo nhiều điều kiện trong pandas, vì sao phải dùng &/| và bọc ngoặc thay vì and/or?
  4. Trình bày mô hình split–apply–combine của groupby. Cho ví dụ tính tổng số tiền theo chi nhánh.
  5. So sánh how="inner"how="left" trong merge; trong bài toán "giữ toàn bộ giao dịch và bổ sung tên khách" nên dùng cái nào?
  6. Vì sao không nên tùy tiện dùng fillna(0) cho cột số tiền giao dịch? Đề xuất cách xử lý hợp lý hơn.

Đọc tiếp

Python 8 — SQL, API & Async

Bài viết liên quan

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 4

Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.

13 thg 7, 2026 3

Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.

13 thg 7, 2026 3

Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.

13 thg 7, 2026 3