Bài 2

Python cho Data Science

Python là ngôn ngữ số một của Data Science nhờ cú pháp gọn và hệ sinh thái thư viện khổng lồ. Bài này đi sâu vào hai công cụ cốt lõi: NumPy (tính toán mảng số) và pandas (thao tác dữ liệu bảng) — kèm nhiều ví dụ code thực tế bạn sẽ dùng mỗi ngày.

Vì sao Python & hệ sinh thái
Một ngôn ngữ, cả pipeline dữ liệu

Python thắng thế trong Data Science vì: cú pháp dễ đọc, cộng đồng lớn, và đặc biệt là hệ sinh thái thư viện phủ toàn bộ vòng đời dữ liệu — từ thu thập, làm sạch, mô hình hoá đến triển khai. Bạn không cần đổi ngôn ngữ giữa các bước.

TầngThư việnDùng để
Tính toán sốNumPyMảng đa chiều, đại số tuyến tính, hiệu năng cao.
Dữ liệu bảngpandasĐọc/biến đổi/gộp dữ liệu dạng bảng.
Trực quan hoámatplotlib, seaborn, plotlyVẽ biểu đồ khám phá & trình bày.
Machine Learningscikit-learnMô hình cổ điển, tiền xử lý, đánh giá.
Deep LearningPyTorch, TensorFlowMạng neuron, dữ liệu ảnh/văn bản.
Môi trườngJupyter, AnacondaNotebook tương tác, quản lý gói.
Ghi nhớ: pandas được xây dựng trên nền NumPy, và scikit-learn nhận đầu vào là mảng NumPy / DataFrame. Nắm vững NumPy + pandas là chìa khoá mở toàn bộ hệ sinh thái.
NumPy: nền tảng tính toán số
ndarray · dtype · vectorization · broadcasting

Đối tượng trung tâm của NumPy là ndarray — mảng N chiều đồng nhất kiểu dữ liệu (dtype). Khác với list Python, ndarray lưu liền kề trong bộ nhớ nên tính toán nhanh hơn hàng chục lần.

Tạo mảng & dtype:

import numpy as np

a = np.array([1, 2, 3])          # từ list, dtype tự suy ra int64
b = np.array([1.0, 2, 3])        # có float -> dtype float64
np.zeros((2, 3))                    # mảng 2x3 toàn 0
np.ones((3,), dtype=np.int32)      # chỉ định dtype
np.arange(0, 10, 2)                 # [0 2 4 6 8]
np.linspace(0, 1, 5)                # 5 điểm cách đều [0, .25, .5, .75, 1]
m = np.arange(12).reshape(3, 4)   # đổi shape thành 3x4

print(m.shape, m.dtype, m.ndim)   # (3, 4) int64 2

Indexing & slicing (0-based; slice trả về view, không copy):

m = np.arange(12).reshape(3, 4)
m[0, 1]          # phần tử hàng 0, cột 1 -> 1
m[1]             # cả hàng 1 -> [4 5 6 7]
m[:, 2]          # cả cột 2 -> [2 6 10]
m[0:2, 1:3]      # khối con 2x2
m[m > 5]          # boolean mask -> [6 7 8 9 10 11]

Vectorization vs vòng lặp: thay vì lặp từng phần tử, thực hiện phép toán trên cả mảng. Code ngắn hơn và nhanh hơn nhiều lần vì chạy ở tầng C.

# Cách CHẬM: vòng lặp Python
x = list(range(1_000_000))
y = [v * 2 + 1 for v in x]

# Cách NHANH: vectorization với NumPy
x = np.arange(1_000_000)
y = x * 2 + 1          # áp cho cả mảng cùng lúc

np.sqrt(x)                # hàm ufunc áp từng phần tử
x.sum(), x.mean(), x.std()

Broadcasting: NumPy tự "kéo giãn" mảng nhỏ để khớp mảng lớn khi shape tương thích, tránh phải nhân bản dữ liệu.

m = np.arange(6).reshape(2, 3)   # shape (2, 3)
row = np.array([10, 20, 30])       # shape (3,)
m + row     # row được broadcast lên từng hàng -> [[10 21 32],[13 24 35]]

col = np.array([[100], [200]])   # shape (2, 1)
m + col     # broadcast theo cột
Quy tắc broadcasting: so khớp shape từ chiều cuối cùng về trước; hai chiều tương thích khi bằng nhau hoặc một trong hai bằng 1.
pandas: Series & DataFrame
Đọc dữ liệu · xem nhanh · loc/iloc · filter

pandas có hai cấu trúc chính: Series (một cột có index) và DataFrame (bảng 2 chiều — tập hợp các Series chung index). Đây là "Excel trong code".

import pandas as pd

s = pd.Series([10, 20, 30], index=["a", "b", "c"])
df = pd.DataFrame({
    "name": ["An", "Binh", "Chi"],
    "age":  [25, 30, 35],
    "city": ["HN", "HCM", "HN"],
})

Đọc dữ liệu & xem nhanh — luôn làm ngay sau khi load:

df = pd.read_csv("data.csv")      # đọc CSV; có read_excel, read_sql, read_json...

df.head(5)        # 5 hàng đầu
df.tail(3)        # 3 hàng cuối
df.shape          # (số hàng, số cột)
df.info()         # kiểu dữ liệu + số non-null từng cột
df.describe()     # thống kê mô tả cột số: count/mean/std/min/quartile/max
df.dtypes         # dtype từng cột
df["city"].value_counts()   # đếm số lần xuất hiện mỗi giá trị

Indexing loc vs iloc — phân biệt rõ để tránh lỗi:

Cú phápÝ nghĩa
df["age"]Chọn một cột (trả về Series).
df[["name","age"]]Chọn nhiều cột (trả về DataFrame).
df.loc[0, "age"]Theo nhãn (label): hàng index 0, cột "age".
df.loc[0:2, ["name","age"]]loc bao gồm cả điểm cuối (0,1,2).
df.iloc[0, 1]Theo vị trí số (integer position): hàng 0, cột 1.
df.iloc[0:2, 0:2]iloc loại trừ điểm cuối (0,1) — giống slice Python.

Lọc boolean (filtering):

df[df["age"] > 28]                        # tuổi > 28
df[(df["age"] > 28) & (df["city"] == "HN")]  # kết hợp điều kiện; dùng & | ~, mỗi vế trong ()
df[df["city"].isin(["HN", "DN"])]        # thuộc tập giá trị
df.query("age > 28 and city == 'HN'")     # cú pháp query gọn hơn
Lưu ý: trong pandas dùng &, |, ~ (không phải and/or/not) khi kết hợp điều kiện, và bắt buộc bọc mỗi điều kiện trong dấu ngoặc vì ưu tiên toán tử.
Biến đổi dữ liệu
assign · apply/map · astype

Tạo cột mới — gán trực tiếp hoặc dùng assign (trả về DataFrame mới, hợp method chaining):

df["age_months"] = df["age"] * 12          # gán trực tiếp

df = df.assign(
    is_adult=df["age"] >= 18,
    age_group=pd.cut(df["age"], bins=[0, 30, 100], labels=["young", "old"]),
)

apply / map — áp hàm lên dữ liệu (ưu tiên vectorization, chỉ dùng khi cần logic phức tạp):

# map: áp hàm/ánh xạ lên từng phần tử của MỘT Series
df["city_full"] = df["city"].map({"HN": "Ha Noi", "HCM": "Ho Chi Minh"})

# apply trên Series: áp hàm từng phần tử
df["name_len"] = df["name"].apply(len)

# apply trên DataFrame theo hàng (axis=1)
df["label"] = df.apply(lambda r: f"{r['name']} ({r['age']})", axis=1)

astype — đổi kiểu dữ liệu (tiết kiệm bộ nhớ, sửa cột đọc sai kiểu):

df["age"] = df["age"].astype("int32")
df["city"] = df["city"].astype("category")   # category: rất tiết kiệm cho cột ít giá trị
df["price"] = pd.to_numeric(df["price"], errors="coerce")  # giá trị lỗi -> NaN
Tổng hợp & kết hợp bảng
groupby · pivot_table · merge · concat

groupby + agg theo mô hình "split – apply – combine": chia nhóm, tính toán trên từng nhóm, gộp kết quả.

# Doanh thu trung bình & tổng theo thành phố
df.groupby("city")["age"].mean()

# Nhiều hàm tổng hợp cùng lúc
df.groupby("city").agg(
    so_nguoi=("name", "count"),
    tuoi_tb=("age", "mean"),
    tuoi_max=("age", "max"),
)
Hàm aggÝ nghĩa
countĐếm số giá trị non-null.
sumTổng.
mean / medianTrung bình / trung vị.
min / maxNhỏ nhất / lớn nhất.
std / varĐộ lệch chuẩn / phương sai.
nuniqueSố giá trị phân biệt.
first / lastPhần tử đầu / cuối mỗi nhóm.

pivot_table — xoay dữ liệu thành bảng chéo (giống Pivot trong Excel):

df.pivot_table(
    values="age",
    index="city",        # hàng
    columns="is_adult",   # cột
    aggfunc="mean",
    fill_value=0,
)

merge / join — nối hai bảng theo khoá chung (giống JOIN trong SQL):

orders.merge(customers, on="customer_id", how="left")
howGiữ lại hàng nào
innerChỉ những khoá có ở cả hai bảng (mặc định).
leftToàn bộ bảng trái + khớp từ bảng phải (thiếu -> NaN).
rightToàn bộ bảng phải + khớp từ bảng trái.
outerToàn bộ khoá ở cả hai bảng (union).

concat — nối chồng nhiều bảng (theo hàng hoặc cột):

pd.concat([df1, df2], axis=0)   # nối theo hàng (thêm dòng)
pd.concat([df1, df2], axis=1)   # nối theo cột (ghép cạnh nhau)
Missing data & dữ liệu thời gian
isna/fillna/dropna · to_datetime · resample

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

df.isna().sum()          # đếm số NaN mỗi cột — bước đầu tiên phải làm

df.dropna()              # bỏ mọi hàng có NaN
df.dropna(subset=["age"])   # chỉ xét NaN ở cột age
df.dropna(axis=1)         # bỏ cột có NaN

df["age"].fillna(df["age"].median())   # điền bằng trung vị
df["city"].fillna("Unknown")             # điền giá trị cố định
df.fillna(method="ffill")                 # điền forward (lấy giá trị phía trước)

Dữ liệu thời gian — chuyển sang datetime rồi khai thác thuộc tính qua .dt:

df["date"] = pd.to_datetime(df["date"])   # parse chuỗi thành datetime

df["year"]    = df["date"].dt.year
df["month"]   = df["date"].dt.month
df["weekday"] = df["date"].dt.day_name()

# resample: nhóm theo chu kỳ thời gian (cần index là datetime)
ts = df.set_index("date")
ts["sales"].resample("M").sum()     # tổng doanh thu theo THÁNG
ts["sales"].resample("W").mean()    # trung bình theo TUẦN
Quy tắc resample: D=ngày, W=tuần, M=cuối tháng, Q=quý, Y=năm, H=giờ. Kết hợp với .sum(), .mean(), .ohlc()...
Hiệu năng & cạm bẫy
Vectorize · SettingWithCopyWarning

Sai lầm phổ biến nhất của người mới là dùng vòng lặp iterrows() để xử lý từng hàng — chậm gấp hàng chục đến hàng trăm lần so với thao tác vectorized.

# CHẬM: iterrows (tránh dùng)
result = []
for idx, row in df.iterrows():
    result.append(row["age"] * 12)
df["age_months"] = result

# NHANH: vectorized
df["age_months"] = df["age"] * 12

# Điều kiện nhiều nhánh: dùng np.where / np.select thay vì apply
import numpy as np
df["tag"] = np.where(df["age"] >= 30, "senior", "junior")
Thứ tự ưu tiên: (1) phép toán vectorized trên cột → (2) np.where/np.select → (3) .map()/.apply() → (cuối cùng, tránh) vòng lặp iterrows.

Cảnh báo kinh điển SettingWithCopyWarning xuất hiện khi bạn gán giá trị lên một "lát cắt" (slice) mà pandas không chắc là view hay copy:

# SAI: chained indexing -> có thể không ghi được & báo warning
df[df["age"] > 30]["city"] = "HN"

# ĐÚNG: dùng .loc một lần duy nhất để chọn hàng & cột
df.loc[df["age"] > 30, "city"] = "HN"

# Khi cần một bảng con độc lập, tạo bản sao rõ ràng
sub = df[df["age"] > 30].copy()
sub["city"] = "HN"   # an toàn, không ảnh hưởng df gốc
Ghi nhớ: luôn dùng df.loc[dieu_kien, cot] = gia_tri để gán, và .copy() khi muốn tách một bảng con độc lập. Đây là lỗi khiến rất nhiều người mất giờ debug.
← Bài trước
Bài 1: Tổng quan