Python 7 — Pandas & NumPy cho phân tích dữ liệu
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 NumPy và pandas, 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 ướcimport numpy as npvàimport 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?
- 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.
- Bộ nhớ liền khối giúp CPU đọc dữ liệu hiệu quả (cache locality).
- 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_dich có ma_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:
| how | Giữ dòng nào | Dùng khi |
|---|---|---|
inner | Chỉ key có ở cả hai bảng | Cần dữ liệu khớp hoàn toàn |
left | Tất cả bảng trái, NaN nếu phải thiếu | Giữ toàn bộ giao dịch |
right | Tất cả bảng phải | Hiếm dùng, thường đảo thành left |
outer | Tấ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ặcmapkhi có thể. Chỉ dùngapplycho 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 đích | NumPy | pandas |
|---|---|---|
| Đơn vị dữ liệu | ndarray | Series, DataFrame |
| Có nhãn cột/index | Không | Có |
| Đa kiểu trong một bảng | Không (đồng nhất) | Có |
| Chọn theo nhãn | — | .loc |
| Chọn theo vị trí | arr[i, j] | .iloc |
| Lọc điều kiện | arr[arr > 0] | df[df["c"] > 0] |
| Gộp nhóm | np.add.reduceat | df.groupby(...).agg(...) |
| Nối bảng | np.concatenate | df.merge(...) |
| Giá trị thiếu | np.nan, np.isnan | NaN, 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ằngdropna/fillna. - Tổng hợp bằng
groupby().agg(), xoay bảng bằngpivot_table, nối bảng bằngmergevới các kiểuinner / left / outer. - Ưu tiên thao tác vector (
np.where,np.select,map) hơnapply(axis=1)để giữ tốc độ; cuối cùng xuất bằngto_csv/to_sql/to_excel.
Tự kiểm tra
- Vì sao tính trên
ndarraybằng vectorization nhanh hơn vòng lặpfortrênlistPython? Nêu ít nhất hai lý do. - Phân biệt
.locvà.iloc; khi viếtdf.loc[0:2]vàdf.iloc[0:2]thì số dòng trả về có khác nhau không? Tại sao? - 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? - 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. - So sánh
how="inner"vàhow="left"trongmerge; 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? - 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
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.
Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.
Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.
Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.