Danh sách bài viết

Bài 38: Chọn dòng / cột với loc và iloc

Truy cập dòng và cột DataFrame với .loc (label-based) và .iloc (position-based). Slice inclusive vs exclusive, chained indexing pitfall, .at / .iat cho scalar, modify an toàn bằng .loc, reset_index sau khi filter.

24/05/2026
13 phút đọc
0 lượt xem
1

Mục tiêu bài học

  • Hiểu khác biệt label-based (.loc) và position-based (.iloc).
  • Nhớ quy tắc slice: .loc INCLUSIVE, .iloc EXCLUSIVE.
  • Tránh chained indexing — gán giá trị qua .loc.
  • Biết .at / .iat cho 1 ô, reset_index sau khi filter.
2

Vì sao cần .loc.iloc

DataFrame có 2 trục đều có thể được "đánh nhãn": index của dòng và tên cột. Khi index là RangeIndex(0..n-1) thì label và position trùng nhau, nên người mới hay nhầm. Khi index là chuỗi ("HN", "HCM") hoặc đã bị "lủng" sau khi filter, label và position khác nhau hẳn — viết df[5] không còn rõ là "dòng có label 5" hay "dòng vị trí thứ 6".

Pandas tách rõ 2 API:

  • .loc[label] — luôn theo label (giá trị index, tên cột).
  • .iloc[position] — luôn theo vị trí integer như list Python.

Dùng đúng accessor giúp code đọc rõ ý đồ và tránh bug khi index không phải số nguyên liên tục.

3

.loc — label-based

Cú pháp: df.loc[row_label, col_label]. Hai phần phân tách bởi dấu phẩy; mỗi phần có thể là 1 label, list label, slice label, hoặc boolean mask.

import pandas as pd

df = pd.DataFrame(
    {
        "name":   ["An", "Binh", "Cuong", "Dung", "Hoa"],
        "age":    [22, 17, 30, 25, 19],
        "salary": [1200, 0, 2500, 1800, 900],
    },
    index=["u1", "u2", "u3", "u4", "u5"],
)

# 1 dòng theo label
df.loc["u3"]

# Slice label — INCLUSIVE cả 2 đầu: lấy u2, u3, u4
df.loc["u2":"u4"]

# Chọn cột
df.loc[:, "name"]                 # 1 cột → Series
df.loc[:, ["name", "age"]]        # nhiều cột → DataFrame

# Row + col
df.loc["u3", "name"]              # 1 ô
df.loc["u2":"u4", ["name", "age"]]

# Boolean mask — chỉ dòng age > 18, lấy cột name
df.loc[df["age"] > 18, "name"]

Điểm hay quên: df.loc["u2":"u4"] lấy cả u4. Slice label inclusive vì label không nhất thiết là số liên tục, không có khái niệm "vị trí kết thúc trừ 1".

4

.iloc — position-based

Cú pháp: df.iloc[row_pos, col_pos]. Vị trí là integer từ 0, hoạt động đúng như slicing list Python — EXCLUSIVE đầu phải.

import pandas as pd

df = pd.DataFrame(
    {
        "name":   ["An", "Binh", "Cuong", "Dung", "Hoa"],
        "age":    [22, 17, 30, 25, 19],
        "salary": [1200, 0, 2500, 1800, 900],
    },
    index=["u1", "u2", "u3", "u4", "u5"],
)

# Dòng đầu (luôn là dòng vị trí 0, bất kể label)
df.iloc[0]            # → label u1

# Slice — EXCLUSIVE: lấy vị trí 0,1,2,3,4 (5 dòng đầu)
df.iloc[0:5]

# Cột đầu tiên
df.iloc[:, 0]

# Row + col theo position
df.iloc[0, 1]                 # 1 ô (dòng 0, cột 1)
df.iloc[1:5, 0:3]             # 4 dòng × 3 cột

# Negative — đếm ngược như list
df.iloc[-1]                   # dòng cuối
df.iloc[:, -1]                # cột cuối

.iloc không quan tâm index hiện tại là gì — luôn dùng vị trí integer. Sau khi filter làm index "lủng" thì .iloc[0] vẫn là dòng đầu của DataFrame mới.

5

So sánh .loc vs .iloc

Bảng tóm tắt:

Đặc tính.loc.iloc
Dựa vàolabel (index, tên cột)position (integer)
SliceINCLUSIVE cả 2 đầuEXCLUSIVE đầu phải (như list)
Boolean maskHỗ trợChỉ nhận ndarray boolean, không nhận Series mask theo label
Negative indexKhông (label "-1" sẽ tra label, không phải vị trí)Có (-1 = dòng cuối)
Khi index là RangeIndex 0..n-1Trùng giá trị với position nhưng vẫn inclusiveNhư cũ — exclusive

Ví dụ trực tiếp khi index là RangeIndex:

import pandas as pd

df = pd.DataFrame({"x": [10, 20, 30, 40, 50]})   # index 0..4

df.loc[1:3]      # label 1,2,3 → 3 dòng (INCLUSIVE)
df.iloc[1:3]     # vị trí 1,2   → 2 dòng (EXCLUSIVE)
6

df["col"] vs df.loc[:, "col"] vs df[0:5]

Một số shorthand Pandas hay gặp:

  • df["col"] — lấy 1 cột thành Series. Tương đương df.loc[:, "col"].
  • df[["col1", "col2"]] — lấy nhiều cột thành DataFrame. Tương đương df.loc[:, ["col1", "col2"]].
  • df[0:5] — slice dòng theo position (EXCLUSIVE), giống df.iloc[0:5]. Đây là exception duy nhất khi [...] không nói về cột.

Khi đọc lại code, viết rõ .loc / .iloc dễ hiểu hơn df[0:5]; shorthand chỉ tiện khi gõ nhanh trong notebook.

7

Chained indexing pitfall

Chained indexing là khi bạn truy cập 2 lần liên tiếp: df["col"][0] hoặc df[df["age"] > 18]["name"] = .... Pandas không đảm bảo kết quả của bước thứ nhất là view hay copy, nên gán giá trị qua chain sẽ ra cảnh báo:

df["col"][0] = 99
# SettingWithCopyWarning: A value is trying to be set on
# a copy of a slice from a DataFrame.

Đôi khi gán "thành công" nhưng thực ra ghi vào copy tạm, DataFrame gốc không đổi. Cách an toàn — luôn dùng .loc trong 1 lần truy cập:

df.loc[0, "col"] = 99                       # gán 1 ô
df.loc[df["age"] > 18, "name"] = "ADULT"    # gán theo mask

Quy tắc đơn giản: khi đọc, chain có thể chấp nhận; khi ghi (assign), luôn 1 lần .loc / .iloc.

8

.at.iat — truy cập scalar

Khi chỉ cần 1 ô (scalar), .at / .iat nhanh hơn .loc / .iloc do bỏ qua bước parse selector phức tạp:

  • df.at[row_label, col_label] — label-based.
  • df.iat[row_pos, col_pos] — position-based.
df.at["u3", "name"]   # nhanh hơn df.loc["u3", "name"]
df.iat[2, 0]          # nhanh hơn df.iloc[2, 0]

df.at["u3", "age"] = 31     # gán cũng OK

Lưu ý: .at / .iat chỉ nhận đúng 1 label / 1 position cho mỗi trục — không chấp nhận list, slice, mask. Dùng khi loop từng dòng (hiếm, vì vectorization gần như luôn nhanh hơn loop) hoặc khi truy cập 1 ô trong thuật toán bước-bước.

9

Modify giá trị qua .loc

.loc không chỉ đọc — đây là cách chuẩn để gán giá trị có điều kiện:

import pandas as pd

df = pd.DataFrame({
    "name": ["An", "Binh", "Cuong", "Dung", "Hoa"],
    "age":  [22, 17, 30, 25, 19],
})

# Gán 1 ô
df.loc[0, "name"] = "Alice"

# Thêm cột mới theo điều kiện
df.loc[df["age"] >= 18, "category"] = "adult"
df.loc[df["age"] < 18,  "category"] = "minor"

# Gán nhiều cột cùng lúc cho 1 dòng
df.loc[2, ["name", "age"]] = ["Charlie", 31]

print(df)

Đây là pattern an toàn nhất khi cần modify, không gặp SettingWithCopyWarning.

10

reset_index sau khi filter

Sau khi df[mask] hoặc df.dropna(), index giữ nguyên label cũ — bị "lủng":

df_adult = df[df["age"] >= 18]
df_adult.index   # Int64Index([0, 2, 3, 4]) — thiếu 1

df_adult.iloc[0]      # dòng đầu của df_adult — vẫn OK
df_adult.loc[1]       # KeyError! label 1 không còn trong df_adult

Nếu sau filter bạn muốn index liên tục 0..k-1 (ví dụ trước khi nối sang code dùng .loc[i] để xử lý batch):

df_adult = df[df["age"] >= 18].reset_index(drop=True)
df_adult.index   # RangeIndex(0..n-1)

Tham số drop=True bỏ luôn cột index cũ; bỏ qua nó pandas sẽ thêm 1 cột tên "index" chứa label cũ.

11

MultiIndex — preview

Khi DataFrame có index nhiều cấp (MultiIndex), .loc nhận tuple key:

import pandas as pd

idx = pd.MultiIndex.from_tuples(
    [("HN", 1), ("HN", 2), ("HCM", 1), ("HCM", 2)],
    names=["city", "store"],
)
df = pd.DataFrame({"revenue": [100, 120, 200, 180]}, index=idx)

df.loc[("HN", 1), "revenue"]   # 100
df.loc["HN"]                   # tất cả dòng có cấp 1 = "HN"
df.loc[("HCM", 2), "revenue"] = 195

MultiIndex dùng khi group by 2+ key (city × store, user × date...). Series sẽ có bài deep dive riêng khi cần.

12

Use case AI / ML

  • Chọn feature cho training: X = df.loc[:, feature_cols], y = df["target"]. Truyền thẳng vào sklearn.
  • Train/test split nhanh theo thứ tự (không random — ví dụ time-series): df_train = df.iloc[:800], df_test = df.iloc[800:].
  • Gán nhãn có điều kiện: df.loc[df["score"] >= 0.5, "pred"] = 1.
  • Lấy ra ví dụ sai khi đánh giá model: errors = df.loc[df["y_true"] != df["y_pred"], ["text", "y_true", "y_pred"]].
13

Code Python tổng hợp

Demo phân biệt label vs position khi index không phải số liên tục:

import pandas as pd

df = pd.DataFrame(
    {
        "name":   ["An", "Binh", "Cuong", "Dung", "Hoa"],
        "age":    [22, 17, 30, 25, 19],
        "salary": [1200,  0, 2500, 1800, 900],
    },
    index=["u1", "u2", "u3", "u4", "u5"],
)

# .loc — label
print(df.loc["u3"])                     # 1 dòng
print(df.loc["u2":"u4"])                # 3 dòng (INCLUSIVE)
print(df.loc[df["age"] >= 18, "name"]) # boolean mask

# .iloc — position
print(df.iloc[0])                       # dòng đầu (label u1)
print(df.iloc[0:3])                     # 3 dòng đầu (EXCLUSIVE)
print(df.iloc[-1])                      # dòng cuối

# Modify an toàn
df.loc[df["age"] >= 18, "category"] = "adult"
df.loc[df["age"] < 18,  "category"] = "minor"
df.loc["u2", "salary"] = 500

# Filter rồi reset_index để index liên tục 0..n-1
adults = df.loc[df["category"] == "adult"].reset_index(drop=True)
print(adults)

# Scalar nhanh — .at / .iat
print(df.at["u3", "name"])     # 'Cuong'
print(df.iat[2, 0])            # 'Cuong'
14

Bài tập

  1. Tạo DataFrame cities với 5 dòng (cột population, gdp), index là tên thành phố ["HN","HCM","DN","HP","CT"]. Dùng .loc["HCM"] in dòng HCM.
  2. Lấy 3 dòng đầu của cities bằng .iloc[:3]. So sánh với cities.loc["HN":"DN"] — kết quả có giống nhau không, vì sao?
  3. Tạo DataFrame employees 10 dòng (cột name, age, salary, index RangeIndex). Lấy cột agesalary cho 5 nhân viên đầu bằng .loc và bằng .iloc.
  4. Với employees ở RangeIndex 0..9, in df.loc[1:5]df.iloc[1:5]. Đếm số dòng trả về của mỗi cái — vì sao khác nhau?
  5. Trên employees, gán cột tier = "high" cho mọi dòng có salary > 1500, ngược lại "low", dùng .loc 2 lần. Verify không có cảnh báo SettingWithCopyWarning.
15

Tóm tắt

  • .loc[label] — label-based, slice INCLUSIVE cả 2 đầu, nhận boolean mask.
  • .iloc[position] — position-based, slice EXCLUSIVE đầu phải, nhận -1 là dòng cuối.
  • df["col"], df[["c1","c2"]] là shorthand chọn cột; df[0:5] là shorthand slice dòng theo position.
  • Khi gán giá trị, luôn 1 lần .loc / .iloc — không chained (df["c"][0] = ...) để tránh SettingWithCopyWarning.
  • .at / .iat chỉ cho scalar 1 ô, nhanh hơn khi gọi nhiều lần.
  • Sau khi filter, gọi .reset_index(drop=True) nếu cần index liên tục.
  • MultiIndex truy cập bằng tuple: df.loc[("HN", 1), "col"].