Danh sách bài viết

Bài 30: Slicing array và boolean indexing

Slicing 1D / 2D / multi-dim của NumPy, view vs copy gotcha, ellipsis và newaxis, boolean indexing với mask, kết hợp điều kiện bằng &, |, ~, np.where và np.nonzero. Code Python ngắn, bài tập cuối bài.

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

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

  • Slicing 1D và multi-dim với cú pháp a[start:stop:step].
  • Phân biệt viewcopy — gotcha hay gặp khi sửa slice ảnh hưởng ngược array gốc.
  • Dùng ... (ellipsis) và np.newaxis cho code đa chiều generic.
  • Lọc dữ liệu bằng boolean indexing và np.where.
  • Áp dụng vào ReLU, lọc theo label, clamp outlier.
2

Slicing 1D

Cú pháp giống list Python: a[start:stop:step]. Mặc định start=0, stop=len(a), step=1. Cả 3 đều bỏ được.

import numpy as np

a = np.arange(10)              # array([0,1,2,3,4,5,6,7,8,9])

a[1:5]                         # array([1, 2, 3, 4])  — từ index 1 đến 4
a[:3]                          # array([0, 1, 2])     — start mặc định 0
a[7:]                          # array([7, 8, 9])     — stop mặc định len(a)
a[::2]                         # array([0, 2, 4, 6, 8]) — step = 2
a[1::2]                        # array([1, 3, 5, 7, 9]) — phần tử ở index lẻ
a[::-1]                        # array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]) — reverse
a[-3:]                         # array([7, 8, 9])     — 3 phần tử cuối

stopexclusive: a[1:5] lấy index 1, 2, 3, 4 (không có 5). Index âm đếm từ cuối: -1 là phần tử cuối.

3

Slicing 2D và multi-dim

NumPy mở rộng slicing list Python sang nhiều chiều: ngăn các axis bằng dấu phẩy. Với 2D: a[rows, cols].

A = np.array([[ 0,  1,  2,  3],
              [ 4,  5,  6,  7],
              [ 8,  9, 10, 11],
              [12, 13, 14, 15]])

A[1:3, 0:2]      # array([[4, 5],
                 #        [8, 9]])      — hàng 1-2, cột 0-1

A[:, 0]          # array([0, 4, 8, 12]) — toàn bộ hàng, cột 0 → 1D
A[0, :]          # array([0, 1, 2, 3])  — hàng 0 → 1D
A[:, 1:3]        # toàn bộ hàng, cột 1-2 (vẫn 2D)
A[::2, ::2]      # array([[ 0,  2],
                 #        [ 8, 10]])    — bước 2 cả 2 chiều

Quy tắc shape: nếu axis dùng integer (như 0) → axis đó bị thu lại, output ít 1 chiều. Nếu axis dùng slice (như 1:3 hay :) → axis đó vẫn còn, output giữ nguyên số chiều.

A[0, :].shape       # (4,)   — int + slice → 1D
A[0:1, :].shape     # (1, 4) — slice + slice → 2D, dù chỉ 1 hàng
A[:, 0].shape       # (4,)
A[:, 0:1].shape     # (4, 1) — column vector
4

View vs Copy — gotcha quan trọng

Khác biệt cốt lõi với list Python: slicing NumPy trả về view, KHÔNG copy. View và array gốc chia sẻ chung buffer dữ liệu — sửa view sẽ sửa luôn array gốc.

a = np.arange(10)              # [0,1,2,3,4,5,6,7,8,9]
b = a[2:6]                     # view, không copy
b[0] = 99                      # sửa b
print(a)                       # [0, 1, 99, 3, 4, 5, 6, 7, 8, 9] ← a bị đổi!
print(b.base is a)             # True — b xài buffer của a

Với list Python thì list[2:6] tạo list mới hoàn toàn. Với NumPy thì không — đây là lý do slicing nhanh và tiết kiệm RAM, nhưng cũng là nguồn bug khi viết hàm trả slice rồi vô tình modify.

Khi cần copy thực sự, gọi .copy():

a = np.arange(10)
b = a[2:6].copy()              # copy độc lập
b[0] = 99
print(a)                       # [0,1,2,3,4,5,6,7,8,9] ← không đổi
print(b.base is None)          # True — b sở hữu buffer riêng

Lưu ý: fancy indexing (lấy bằng list index hoặc boolean mask, xem bước 7) luôn trả về copy, không phải view. Chỉ slicing kiểu a[start:stop:step] mới là view.

5

Ellipsis ...

... (Ellipsis) thay cho "tất cả các axis còn lại bằng :". Hữu ích khi không biết số chiều cụ thể.

X = np.random.rand(2, 3, 4, 5)   # shape (2, 3, 4, 5)

X[..., 0].shape     # (2, 3, 4)  — tương đương X[:, :, :, 0]
X[0, ..., 0].shape  # (3, 4)     — tương đương X[0, :, :, 0]
X[..., 0, :].shape  # (2, 3, 5)  — tương đương X[:, :, 0, :]

Mỗi expression chỉ dùng được 1 ellipsis. Use case điển hình: ảnh có shape (H, W, C) hoặc (B, H, W, C), lấy channel cuối thì viết img[..., 0] chạy đúng cho cả 2 trường hợp.

6

newaxis — thêm chiều

np.newaxis (alias của None) thêm 1 chiều có size 1 vào vị trí chỉ định. Hay dùng để chuẩn bị shape cho broadcasting (sẽ học ở Bài 33).

v = np.array([1, 2, 3])        # shape (3,)

v[:, np.newaxis].shape         # (3, 1) — column vector
v[np.newaxis, :].shape         # (1, 3) — row vector
v[:, None].shape               # (3, 1) — None là alias của np.newaxis

# Hoặc dùng reshape:
v.reshape(3, 1).shape          # (3, 1)

Khi nào dùng newaxis: cần broadcast 1 vector (n,) với ma trận (n, m) — phải đổi thành (n, 1) trước.

7

Boolean indexing

So sánh element-wise tạo boolean array (mask) cùng shape. Dùng mask để lọc phần tử thỏa điều kiện.

a = np.array([-3, -1, 0, 2, 5, 8])

mask = a > 0                   # array([False, False, False, True, True, True])
a[mask]                        # array([2, 5, 8])

# Inline, không cần biến phụ:
a[a > 0]                       # array([2, 5, 8])

Lưu ý quan trọng: kết quả của boolean indexing luôn là 1D flat, bất kể array đầu vào bao nhiêu chiều.

A = np.array([[1, -2, 3],
              [-4, 5, -6]])
A[A > 0]                       # array([1, 3, 5]) — 1D, mất shape gốc

Boolean indexing luôn trả về copy (không phải view), nên sửa kết quả không ảnh hưởng array gốc — trừ khi assignment ngược (xem bước 11).

8

Kết hợp boolean conditions

Với NumPy boolean array, KHÔNG dùng and, or, not — sẽ raise ValueError: The truth value of an array ... is ambiguous. Dùng các operator bitwise:

  • & — and
  • | — or
  • ~ — not
  • ^ — xor
a = np.arange(20)

a[(a > 5) & (a < 15)]           # array([6,7,8,9,10,11,12,13,14])
a[(a < 3) | (a > 17)]           # array([0, 1, 2, 18, 19])
a[~(a % 2 == 0)]                # array([1,3,5,7,9,11,13,15,17,19]) — số lẻ

Bắt buộc dấu ngoặc quanh từng điều kiện: &| có precedence cao hơn >, <, nên a > 5 & a < 15 được Python parse thành a > (5 & a) < 15 → sai.

9

np.where — vectorized if-else

np.where(condition, x, y) trả về phần tử từ x ở những vị trí condition True, từ y ở những vị trí False. Giữ nguyên shape input.

a = np.array([-3, -1, 0, 2, 5])

# Manual ReLU: max(x, 0)
np.where(a > 0, a, 0)          # array([0, 0, 0, 2, 5])

# Clip về [0, 3]
np.where(a > 3, 3, np.where(a < 0, 0, a))   # array([0, 0, 0, 2, 3])

# Đánh nhãn theo điều kiện
np.where(a > 0, 'pos', 'non-pos')
# array(['non-pos','non-pos','non-pos','pos','pos'], dtype='<U7')

Khi gọi với 1 đối số, np.where(condition) trả về tuple các indices của vị trí True (tương đương np.nonzero(condition)).

a = np.array([0, 5, 0, 3, 0, 8])
np.where(a > 0)                # (array([1, 3, 5]),) — tuple chứa 1 array
np.where(a > 0)[0]             # array([1, 3, 5])
10

np.nonzero — indices khác 0

np.nonzero(arr) trả về tuple các index của phần tử khác 0. Với array N chiều, tuple có N array — mỗi array là indices theo 1 axis.

a = np.array([0, 3, 0, 5, 7, 0])
np.nonzero(a)                  # (array([1, 3, 4]),)

A = np.array([[0, 2, 0],
              [3, 0, 4]])
np.nonzero(A)
# (array([0, 1, 1]), array([1, 0, 2]))
# → các vị trí khác 0 là (0,1), (1,0), (1,2)

A[np.nonzero(A)]               # array([2, 3, 4]) — lấy giá trị

Với boolean array, np.nonzero(mask) trả về indices của True — bằng np.where(mask).

11

Modify qua boolean indexing

Boolean mask ở phía bên trái = dùng để gán giá trị có điều kiện in-place — viết ngắn gọn hơn vòng for nhiều.

a = np.array([-3, -1, 0, 2, 5, -8, 7])

a[a < 0] = 0                   # clamp âm về 0 (manual ReLU in-place)
# a = array([0, 0, 0, 2, 5, 0, 7])

# Clip outlier 2 phía
x = np.array([0.1, 0.5, 1.2, 0.8, 2.5, -0.3])
x[x > 1.0] = 1.0
x[x < 0.0] = 0.0
# x = array([0.1, 0.5, 1.0, 0.8, 1.0, 0.0])
# tương đương np.clip(x, 0.0, 1.0)

Có thể gán array cùng số phần tử với mask:

a = np.array([1, 2, 3, 4, 5])
a[a % 2 == 0] = [-2, -4]       # thay 2 và 4 bằng -2, -4
# a = array([ 1, -2,  3, -4,  5])
12

Use case AI

1. Lọc sample theo label. X là feature matrix shape (n_samples, n_features), y là label vector shape (n_samples,):

X_class1 = X[y == 1]            # tất cả sample thuộc class 1
X_pos    = X[y > 0]             # sample có label dương

2. ReLU và Leaky ReLU manual.

def relu(x):
    return np.where(x > 0, x, 0)

def leaky_relu(x, alpha=0.01):
    return np.where(x > 0, x, alpha * x)

3. Mask cho missing values. Trong feature engineering, NaN đại diện missing. Tạo mask bằng np.isnan:

x = np.array([1.0, np.nan, 3.0, np.nan, 5.0])
missing = np.isnan(x)           # array([F, T, F, T, F])
x[missing] = x[~missing].mean() # fill bằng mean của phần không missing

4. Đếm sample thỏa điều kiện.True = 1, False = 0, sum của boolean array đếm số True:

accuracy = (y_pred == y_true).mean()   # tỷ lệ dự đoán đúng
n_positive = (y == 1).sum()
13

Code Python tổng hợp

import numpy as np

# ----- Slicing 1D -----
a = np.arange(10)
print(a[2:7])           # [2 3 4 5 6]
print(a[::-1])          # reverse: [9 8 7 6 5 4 3 2 1 0]
print(a[::3])           # step 3:  [0 3 6 9]

# ----- Slicing 2D -----
A = np.arange(16).reshape(4, 4)
print(A[1:3, 0:2])      # block 2x2: [[4 5] [8 9]]
print(A[:, 0])          # cột 0: [ 0  4  8 12]
print(A[0, :])          # hàng 0: [0 1 2 3]

# ----- View vs Copy gotcha -----
a = np.arange(5)
b = a[1:4]              # view
b[0] = 999
print(a)                # [0 999 2 3 4] ← a bị đổi!

c = a[1:4].copy()       # copy độc lập
c[0] = -1
print(a)                # [0 999 2 3 4] ← a không đổi

# ----- Boolean indexing -----
x = np.array([-3, -1, 0, 2, 5, -8, 7])
print(x[x > 0])                       # [2 5 7]
print(x[(x > -2) & (x < 3)])           # [-1  0  2]

# ----- np.where: relu manual -----
print(np.where(x > 0, x, 0))          # [0 0 0 2 5 0 7]

# ----- Modify in-place qua mask -----
x[x < 0] = 0
print(x)                              # [0 0 0 2 5 0 7]

# ----- newaxis: 1D → column vector -----
v = np.array([1, 2, 3])
print(v[:, None].shape)               # (3, 1)
14

Bài tập

Bài 1. Cho a = np.arange(1, 21) (1 đến 20). Dùng slicing với step để lấy mảng các phần tử chẵn. Sau đó làm lại bằng boolean indexing và so sánh kết quả.

Bài 2. Cho ma trận M = np.arange(16).reshape(4, 4). Lấy đường chéo chính (diagonal) — gồm các phần tử M[0,0], M[1,1], M[2,2], M[3,3] — bằng slicing với step. Gợi ý: dùng flat hoặc tính step dựa trên số cột.

Bài 3. Cho x = np.array([-2.5, -0.5, 0.0, 1.2, 3.0, -4.1, 5.5]). Clip các phần tử âm về 0 bằng boolean indexing in-place. Kết quả mong đợi: [0. 0. 0. 1.2 3. 0. 5.5].

Bài 4. Implement Leaky ReLU bằng np.where: f(x) = x nếu x > 0, ngược lại 0.01 * x. Test với x = np.array([-5.0, -1.0, 0.0, 1.0, 5.0]); kết quả mong đợi [-0.05, -0.01, 0., 1., 5.].

Bài 5. Cho y = np.array([0, 1, 0, 1, 1, 0, 1])X = np.arange(14).reshape(7, 2). Lấy submatrix gồm các hàng của X ứng với y == 1. Dùng .shape để xác nhận kích thước output.

Bài 6 (gotcha). Viết hàm get_first_row(A) trả về A[0]. Test xem nếu caller làm row = get_first_row(A); row[0] = 999 thì A có bị đổi không? Sửa lại hàm để an toàn.

15

Tóm tắt

  • Slicing a[start:stop:step] với multi-axis ngăn bằng dấu phẩy: A[1:3, 0:2], A[:, 0].
  • Slicing trả về view chia sẻ buffer với gốc — sửa view sửa luôn gốc. Dùng .copy() khi cần độc lập.
  • Boolean indexing và fancy indexing luôn trả copy, không phải view.
  • ... thay cho "tất cả các axis còn lại"; np.newaxis (hay None) thêm 1 chiều size 1.
  • Mask kết hợp bằng &, |, ~ (không phải and, or, not), nhớ dấu ngoặc quanh từng điều kiện.
  • np.where(cond, x, y) là vectorized if-else (giữ shape); np.where(cond) hay np.nonzero trả indices.
  • Gán in-place qua mask: a[a < 0] = 0 — viết clamp / fill missing / clip outlier ngắn gọn.