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.
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ầng | Thư viện | Dùng để |
|---|---|---|
| Tính toán số | NumPy | Mảng đa chiều, đại số tuyến tính, hiệu năng cao. |
| Dữ liệu bảng | pandas | Đọc/biến đổi/gộp dữ liệu dạng bảng. |
| Trực quan hoá | matplotlib, seaborn, plotly | Vẽ biểu đồ khám phá & trình bày. |
| Machine Learning | scikit-learn | Mô hình cổ điển, tiền xử lý, đánh giá. |
| Deep Learning | PyTorch, TensorFlow | Mạng neuron, dữ liệu ảnh/văn bản. |
| Môi trường | Jupyter, Anaconda | Notebook tương tác, quản lý gói. |
Đố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
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
&, |, ~ (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ử.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
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. |
sum | Tổng. |
mean / median | Trung bình / trung vị. |
min / max | Nhỏ nhất / lớn nhất. |
std / var | Độ lệch chuẩn / phương sai. |
nunique | Số giá trị phân biệt. |
first / last | Phầ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")
how | Giữ lại hàng nào |
|---|---|
inner | Chỉ những khoá có ở cả hai bảng (mặc định). |
left | Toàn bộ bảng trái + khớp từ bảng phải (thiếu -> NaN). |
right | Toàn bộ bảng phải + khớp từ bảng trái. |
outer | Toà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)
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
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()...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")
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
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.