Không gian tự học dành cho sinh viên UET

Chào mừng đến với UETệ

Tìm bài giảng theo học phần, hiểu lại những kiến thức nền và ôn tập theo nhịp học của bạn.

Bài giảng
Đọc Notes, ôn nhanh với Slides và luyện tập qua các bài có lời giải.
Wiki kiến thức
Tra cứu thuật ngữ và bổ sung kiến thức nền ngay khi cần.
Góc học tập
Ôn bằng flashcard, lưu ghi chú và hẹn giờ tập trung.

Bạn có thể xem lại phần giới thiệu ở cuối trang.

Buy me a coffee

Website hoàn toàn miễn phí. Nếu thấy hữu ích, bạn có thể tặng tôi một cốc cà phê theo thông tin dưới đây nhé!

Mã QR chuyển khoản Momo
Ngân hàng
Momo
Tên người nhận
MOMO - TKTH DO MINH TRI
Số tài khoản
01MMTTT0060405229
Skip to content

Mảng NumPy và tính toán trên mảng

Bài 4Cập nhật 18/10/2026Ghi chú của bạn

1. Cấu trúc mảng đa chiều ndarray trong NumPy ​

Bạn cần trừ cùng một giá trị hiệu chuẩn khỏi một dãy số đo nhiệt độ. Viết một vòng lặp Python là cách trực tiếp, nhưng khi dãy dài, việc xử lý từng đối tượng có thể tốn thêm thời gian. NumPy cung cấp ndarray để tổ chức mảng và gọi các phép toán số học trên cả mảng. Bài này giải thích cách lưu trữ, cách chọn phần tử và những điều kiện giúp phép toán đó giảm công việc ở tầng Python.

1.1. So sánh cơ chế bộ nhớ của Python list và ndarray ​

Trước hết cần phân biệt các giá trị được lưu trực tiếp với các tham chiếu tới đối tượng:

  • Danh sách Python (list): Trong CPython, giữ một dãy tham chiếu tới các đối tượng. Nhiều vị trí có thể cùng trỏ tới một đối tượng. Khi duyệt, chương trình truy cập các đối tượng qua tham chiếu, nên kích thước và cách dùng chung đối tượng đều ảnh hưởng đến bộ nhớ thực tế.
  • Mảng NumPy (ndarray): Với dtype số thông thường, vùng dữ liệu lưu trực tiếp các giá trị theo cùng một dtype. Mảng int64 liên tục theo thứ tự C đặt các số liền nhau, mỗi số chiếm 8 byte. Tuy nhiên, một view có thể truy cập vùng đệm bằng bước nhảy khác, và dtype object vẫn lưu tham chiếu tới đối tượng Python.

Số byte của các phần tử nhìn thấy trong một mảng được tính bằng:

nbytes=size×itemsize

Một triệu phần tử int64 có nbytes bằng 8.000.000 byte, tức 8 MB theo hệ thập phân, hoặc khoảng 7,63 MiB. Con số này chưa gồm siêu dữ liệu. Với view, nbytes cũng không cho biết lượng bộ nhớ mới được cấp phát. Muốn so với phải đo trong cùng môi trường và tránh đếm lặp các đối tượng dùng chung.

1.2. Khởi tạo mảng ndarray ​

NumPy cung cấp nhiều phương thức khởi tạo mảng:

python
import numpy as np

# Tạo mảng từ danh sách
arr1 = np.array([1, 2, 3, 4])

# Tạo mảng toàn số không hoặc số một
zeros_arr = np.zeros((3, 4), dtype=np.float64)  # 3 hàng, 4 cột
ones_arr = np.ones((2, 3), dtype=np.int32)

# Tạo mảng chưa khởi tạo giá trị (nhận giá trị rác trong RAM)
empty_arr = np.empty((2, 2))

# Tạo dãy số cách đều
range_arr = np.arange(10, 30, 5)  # array([10, 15, 20, 25])

# Tạo ma trận đơn vị
eye_arr = np.eye(3)

1.3. Kiểu dữ liệu (dtype) và cạm bẫy tràn số nguyên ​

Mỗi mảng ndarray có một thuộc tính dtype quy định kiểu dữ liệu đồng nhất của mọi phần tử trong mảng:

  • Kiểu số nguyên có dấu: int8 (−128 đến 127), int16, int32, int64.
  • Kiểu số nguyên không dấu: uint8 (0 đến 255), uint16, uint32, uint64.
  • Kiểu số thực: float16, float32, float64 (chuẩn IEEE 754).
  • Kiểu logic: bool (True hoặc False, chiếm 1 byte mỗi phần tử).

Chuyển đổi kiểu dữ liệu được thực hiện qua phương thức .astype(). Theo mặc định, phương thức này tạo ra một bản sao mới trong bộ nhớ (copy=True). Nếu truyền copy=False, NumPy chỉ trả về chính mảng ban đầu khi kiểu dữ liệu và thứ tự sắp xếp bộ nhớ đã hoàn toàn trùng khớp, ngược lại phương thức vẫn tạo một bản sao mới:

python
arr_float = np.array([1.2, 3.8, -2.5])
arr_int = arr_float.astype(np.int32)  # array([1, 3, -2]) - cắt bỏ phần thập phân

Cạm bẫy thời điểm ép kiểu và tràn số nguyên

Trong kiểu số nguyên cố định byte (như int8), nếu một phép toán vượt ra ngoài giới hạn cho phép, giá trị sẽ bị tràn nhị phân âm thầm mà không ném lỗi. Nếu ta thực hiện phép tính trước rồi mới ép kiểu, kết quả đã bị sai lệch từ trước:

python
x = np.array([50, 100, 120], dtype=np.int8)
a = (x * 2).astype(np.int16)  # Sai: Tràn số trong int8 trước khi đổi kiểu!
b = x.astype(np.int16) * 2     # Đúng: Đổi kiểu sang int16 rồi mới nhân đôi

1.4. Phép toán số học vector hóa trên mảng ​

Khi áp dụng các phép toán số học (+, -, *, /, **) giữa các mảng cùng kích thước hoặc giữa mảng với một số vô hướng, NumPy thực thi phép tính theo từng phần tử (element-wise):

python
arr = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(arr * arr)
# [[ 1.  4.  9.]
#  [16. 25. 36.]]

print(1 / arr)
print(arr ** 0.5)

Vòng lặp số học được thực hiện trong mã thư viện đã biên dịch thay vì lặp từng bước ở tầng Python. Một số phép toán trên phần cứng phù hợp còn có thể dùng SIMD để xử lý nhiều phần tử trong một lệnh máy. Điều này không bảo đảm mọi phép toán dùng SIMD hoặc nhiều luồng.

1.5. Chỉ mục cơ bản, cắt lát và bản chất của View ​

Truy xuất mảng 2 chiều trong NumPy sử dụng cặp chỉ số ngăn cách bởi dấu phẩy arr[hang, cot], thay vì hai lần ngoặc vuông arr[hang][cot] như danh sách lồng nhau của Python.

Điểm khác biệt căn bản giữa Python list và NumPy ndarray:

  • Cắt lát trên danh sách Python luôn tạo ra một bản sao mới (copy).
  • Cắt lát trên mảng NumPy tạo ra một khung nhìn (View). View không cấp phát vùng đệm dữ liệu mới mà dùng chung vùng nhớ với mảng gốc.
python
A = np.array([10, 20, 30, 40, 50])
v = A[1:4]   # v là một View trỏ vào các phần tử 20, 30, 40
v[0] = 999   # Sửa phần tử đầu tiên của v
print(A)     # [ 10, 999,  30,  40,  50] -> Mảng gốc A bị thay đổi!

Nếu bạn muốn chỉnh sửa trên lát cắt mà không ảnh hưởng tới mảng ban đầu, bắt buộc phải sao chép dữ liệu tường minh bằng phương thức .copy():

python
v_doc_lap = A[1:4].copy()

1.6. Cơ chế bộ nhớ của reshape: Phân biệt View và Copy ​

Một câu hỏi cốt lõi trong kỹ thuật dữ liệu: Khi gọi phương thức .reshape(), kết quả thu được là một View dùng chung bộ nhớ hay là một bản sao Copy mới?

NumPy quản lý cấu trúc mảng thông qua hai thuộc tính:

  • shape: Tuple chứa kích thước của từng chiều.
  • strides: Tuple chứa số byte cần dịch chuyển trong bộ nhớ để bước sang phần tử kế tiếp của từng chiều.

Ví dụ dùng chung bộ nhớ (Sharing Memory - View) ​

Xét mảng hai chiều A gồm 12 số nguyên 64-bit (3×4), được lưu trữ liên tục theo hàng (C-contiguous):

python
A = np.arange(12).reshape(3, 4)
# A có shape=(3, 4) và strides=(32, 8) (bước hàng là 4*8=32 byte, bước cột là 8 byte)

B = A.reshape(4, 3)

Vì dữ liệu gốc vốn đã nằm liên tiếp trong bộ nhớ theo thứ tự 0,1,…,11, NumPy có thể thỏa mãn hình dạng mới (4,3) chỉ bằng cách tạo một đối tượng metadata mới với shape=(4, 3) và strides=(24, 8). Không có byte dữ liệu nào bị sao chép. Ta kiểm chứng việc dùng chung vùng đệm bộ nhớ bằng hàm:

python
np.shares_memory(A, B)  # Trả về True

Ví dụ không dùng chung bộ nhớ (Non-Sharing Memory - Copy) ​

Xét mảng chuyển vị của A:

python
T = A.T  # T có shape=(4, 3) nhưng strides=(8, 32)

Mảng T là một View của A, nhưng các phần tử theo thứ tự đọc hàng của nó không còn nằm liên tiếp trong bộ nhớ vật lý. Thuộc tính T.flags['C_CONTIGUOUS'] trả về False.

Nếu bây giờ ta yêu cầu làm phẳng mảng T thành một mảng một chiều:

python
C = T.reshape(-1)

NumPy không thể biểu diễn một mảng một chiều đọc theo hàng từ T chỉ bằng một bước nhảy stride cố định trên vùng nhớ phân tán đó. Do đó, NumPy bắt buộc phải cấp phát một vùng nhớ mới trong RAM và sao chép toàn bộ các giá trị vào đó theo thứ tự mới:

python
np.shares_memory(T, C)  # Trả về False

Quy tắc kiểm chứng vùng nhớ chuẩn mực

Để kiểm tra hai mảng có dùng chung bộ nhớ hay không, luôn sử dụng hàm np.shares_memory(arr1, arr2). Tuyệt đối không dùng phép so sánh C.base is A. Thuộc tính .base có thể trỏ tới một đối tượng trung gian hoặc trả về None, không phản ánh chính xác trạng thái đệm dữ liệu. Đồng thời, không khẳng định mọi mảng ndarray đều liên tục trong bộ nhớ. Các lát cắt có bước nhảy khác một (A[::2]) hoặc mảng chuyển vị (A.T) là các mảng không liên tục (non-contiguous).

1.7. Chỉ mục boolean ​

Chỉ mục boolean cho phép trích xuất các phần tử thỏa mãn một biểu thức điều kiện vị từ:

python
ten = np.array(["An", "Bình", "An", "Cường"])
diem = np.array([[8, 9], [5, 6], [7, 10], [4, 5]])

# Tạo mặt nạ boolean
mask = ten == "An"  # array([True, False, True, False])

# Lọc các hàng tương ứng với mặt nạ
print(diem[mask])
# [[ 8  9]
#  [ 7 10]]

Khi kết hợp nhiều điều kiện logic, bắt buộc phải dùng các toán tử bit:

  • Toán tử VÀ: &
  • Toán tử HOẶC: |
  • Toán tử PHỦ ĐỊNH: ~ Mỗi điều kiện con phải được bọc trong cặp ngoặc đơn () để tránh lỗi ưu tiên toán tử:
python
mask_hop = (ten == "An") | (ten == "Cường")

Gán giá trị qua mặt nạ boolean giúp biến đổi trực tiếp các phần tử thỏa mãn điều kiện:

python
diem[diem < 5] = 5  # Mọi điểm dưới 5 được nâng lên 5

1.8. Chỉ mục mảng số nguyên (Fancy Indexing) ​

Fancy Indexing là thuật ngữ chỉ việc sử dụng một mảng hoặc danh sách các số nguyên để chọn các phần tử theo thứ tự bất kỳ:

python
arr = np.zeros((8, 4))
for i in range(8):
    arr[i] = i

# Chọn các hàng 4, 3, 0 theo đúng thứ tự chỉ định
chon = arr[[4, 3, 0]]

Fancy Indexing luôn tạo bản sao

Khác với cắt lát thông thường, Fancy Indexing luôn luôn sao chép dữ liệu sang một vùng nhớ mới, hoàn toàn không bao giờ tạo ra View. Mọi thao tác gán lên mảng kết quả của Fancy Indexing sẽ không làm ảnh hưởng tới mảng gốc trừ khi thực hiện phép gán trực tiếp: arr[[4, 3]] = 999.

Để chọn một vùng chữ nhật gồm các hàng và cột giao nhau, ta sử dụng hàm hỗ trợ np.ix_:

python
arr = np.arange(16).reshape((4, 4))
# Chọn hàng 1, 3 và cột 0, 2
vung_chon = arr[np.ix_([1, 3], [0, 2])]
# [[ 4  6]
#  [12 14]]

1.9. Chuyển vị mảng và hoán đổi trục ​

Chuyển vị là một trường hợp đặc biệt của việc tạo View:

  • Thuộc tính .T: Hoán đổi hai trục của mảng 2 chiều.
  • Phương thức arr.swapaxes(truc1, truc2): Hoán đổi hai trục chỉ định trên mảng nhiều chiều.

Chuyển vị chỉ thay đổi thuộc tính shape và đảo thứ tự của strides, hoàn toàn không tốn thời gian di chuyển dữ liệu trong RAM.


2. Sinh số ngẫu nhiên giả ​

Mô-đun numpy.random cung cấp các thuật toán sinh số ngẫu nhiên tối ưu hóa cho mảng nhiều chiều. Từ phiên bản NumPy hiện đại, giao diện bộ sinh ngẫu nhiên default_rng được khuyến nghị sử dụng:

python
# Khởi tạo bộ sinh số ngẫu nhiên với hạt giống cố định
rng = np.random.default_rng(seed=42)

# Sinh mảng phân phối chuẩn tắc (trung bình 0, độ lệch chuẩn 1)
chuan = rng.standard_normal(size=(3, 3))

# Sinh số thực phân phối đều trong đoạn [0, 1)
deu = rng.uniform(low=0.0, high=10.0, size=5)

# Sinh số nguyên ngẫu nhiên trong khoảng [a, b)
nguyen = rng.integers(low=1, high=100, size=(2, 4))

# Lấy mẫu ngẫu nhiên không lặp lại từ một mảng có sẵn
tap_goc = np.array([10, 20, 30, 40, 50, 60])
mau = rng.choice(tap_goc, size=3, replace=False)

Tham số replace=False bảo đảm mỗi phần tử chỉ được chọn tối đa một lần. Việc cố định hạt giống ngẫu nhiên seed giúp kết quả tính toán có tính tái lập (reproducibility), hỗ trợ kiểm thử và chia sẻ mã nguồn.


3. Các hàm phổ quát ufunc ​

Hàm phổ quát (Universal Function hay ufunc) là một hàm thực thi các phép toán trên từng phần tử của mảng ndarray. ufunc được biên dịch trực tiếp bằng C, có khả năng ép kiểu tự động và hỗ trợ phát sóng (broadcasting).

3.1. Các hàm ufunc một ngôi ​

Tiếp nhận một mảng đầu vào và trả về một mảng kết quả cùng kích thước:

  • np.abs(), np.fabs(): Giá trị tuyệt đối của từng phần tử.
  • np.sqrt(): Căn bậc hai (tương đương arr ** 0.5).
  • np.square(): Bình phương từng phần tử.
  • np.exp(): Hàm mũ ex.
  • np.log(), np.log10(), np.log2(): Logarit tự nhiên, cơ số 10 và cơ số 2.
  • np.sign(): Trả về dấu của số: 1 (dương), 0 (bằng không), và −1 (âm).
  • np.ceil(), np.floor(): Làm tròn lên và làm tròn xuống số nguyên gần nhất.
  • np.isnan(): Trả về mảng boolean đánh dấu các giá trị không xác định NaN (Not a Number).
  • np.isfinite(): Kiểm tra các giá trị hữu hạn (không phải NaN và không phải dương hoặc âm vô cực).

3.2. Các hàm ufunc hai ngôi ​

Tiếp nhận hai mảng đầu vào và thực hiện phép toán theo từng cặp phần tử:

  • np.add(), np.subtract(), np.multiply(), np.divide(): Cộng, trừ, nhân, chia mảng.
  • np.maximum(), np.minimum(): Tìm giá trị lớn nhất và nhỏ nhất giữa từng cặp phần tử tương ứng của hai mảng.
  • np.copysign(): Sao chép dấu của mảng thứ hai sang các giá trị của mảng thứ nhất.
  • np.mod(): Phép chia lấy phần dư theo từng phần tử.

4. Lập trình hướng mảng ​

Lập trình hướng mảng là phong cách thay thế các vòng lặp duyệt phần tử bằng các biểu thức mảng ngắn gọn.

4.1. Biểu diễn logic điều kiện với np.where ​

Hàm np.where(dieu_kien, neu_dung, neu_sai) là phiên bản vector hóa của biểu thức ba ngôi x if dieu_kien else y:

python
diem = np.array([4.5, 7.0, 8.5, 3.0, 9.0])
ket_qua = np.where(diem >= 5.0, "Đạt", "Trượt")
# array(['Trượt', 'Đạt', 'Đạt', 'Trượt', 'Đạt'], dtype='<U5')

Ta cũng có thể truyền mảng số học vào các nhánh để tính toán giá trị tương ứng:

python
# Nếu điểm dưới 5 thì cộng 1 điểm khuyến khích, ngược lại giữ nguyên
diem_moi = np.where(diem < 5.0, diem + 1.0, diem)

4.2. Các phương thức toán học và thống kê ​

NumPy hỗ trợ các hàm thống kê tổng hợp trên toàn bộ mảng hoặc dọc theo một trục xác định:

Phương thứcÝ nghĩa toán học
sum()Tổng toàn bộ các phần tử
mean()Trung bình cộng số học
std(), var()Độ lệch chuẩn và phương sai
min(), max()Giá trị nhỏ nhất và lớn nhất
argmin(), argmax()Vị trí chỉ số của phần tử nhỏ nhất và lớn nhất
cumsum()Tổng tích lũy từ phần tử đầu tiên
cumprod()Tích tích lũy từ phần tử đầu tiên

Tham số axis và keepdims ​

Khi tính toán trên mảng 2 chiều:

  • axis=0: Tổng hợp dọc theo các hàng (tính kết quả cho từng cột).
  • axis=1: Tổng hợp ngang qua các cột (tính kết quả cho từng hàng).
python
M = np.array([[10, 12, 11],
              [20, 21, 24],
              [30, 33, 31]])

# Tính trung bình từng hàng
tb_hang = M.mean(axis=1, keepdims=True)
# shape của tb_hang là (3, 1), cho phép trừ trực tiếp M - tb_hang bằng broadcasting

4.3. Các phương thức cho mảng boolean ​

Trên mảng boolean, các giá trị True được coi là 1 và False được coi là 0:

  • Đếm số lượng phần tử thỏa mãn điều kiện:
python
so_luong_lon = (M > 20).sum()  # Đếm có bao nhiêu ô lớn hơn 20
  • arr.any(): Kiểm tra xem có ít nhất một giá trị True hay không.
  • arr.all(): Kiểm tra xem toàn bộ các giá trị có phải là True hay không.

4.4. Sắp xếp mảng ​

  • Sắp xếp tại chỗ: Phương thức arr.sort(axis=...) làm thay đổi trực tiếp mảng gốc.
  • Sắp xếp trả về bản sao: Hàm np.sort(arr) giữ nguyên mảng gốc và trả về mảng mới.
  • Tìm chỉ số sắp xếp: Hàm np.argsort(arr) trả về mảng các chỉ số nguyên quy định thứ tự sắp xếp tăng dần, thuận tiện khi cần sắp xếp đồng thời nhiều mảng dữ liệu liên quan.

4.5. Đại số tập hợp trên mảng ​

NumPy cung cấp các hàm tập hợp 1 chiều xử lý mảng với tốc độ vượt trội:

  • np.unique(arr): Trả về các giá trị phân biệt trong mảng và đã được sắp xếp tăng dần.
  • np.isin(arr, tap_hop): Kiểm tra từng phần tử của mảng có thuộc một tập hợp giá trị cho trước hay không, trả về mảng boolean.
  • np.intersect1d(x, y): Tìm phần giao của hai mảng.
  • np.union1d(x, y): Tìm phần hợp của hai mảng.
  • np.setdiff1d(x, y): Tìm phần hiệu (các phần tử có trong x nhưng không có trong y).

5. Nhập và xuất tệp với mảng ​

NumPy sở hữu định dạng nhị phân chuyên dụng .npy và .npz để lưu trữ dữ liệu mảng trực tiếp xuống đĩa, bảo toàn toàn bộ cấu trúc shape và dtype mà không bị sai số làm tròn như tệp văn bản CSV:

python
arr = np.arange(10)

# Lưu một mảng đơn lẻ ra tệp .npy
np.save("mang_luu.npy", arr)

# Nạp mảng từ tệp .npy
arr_doc = np.load("mang_luu.npy")

# Lưu nhiều mảng vào tệp nén .npz
np.savez_compressed("du_lieu_kho.npz", a=arr, b=arr*2)

# Nạp tệp .npz (truy xuất dạng từ điển)
du_lieu = np.load("du_lieu_kho.npz")
print(du_lieu["a"])
print(du_lieu["b"])

6. Đại số tuyến tính ​

Các phép toán ma trận trong NumPy được ủy thác cho thư viện Fortran tiêu chuẩn cao (BLAS/LAPACK):

6.1. Nhân ma trận với toán tử @ ​

Toán tử @ (được giới thiệu từ Python 3.5) biểu diễn phép nhân ma trận chuẩn mực, thay thế cho hàm np.dot():

python
X = np.array([[1.0, 2.0], [3.0, 4.0]])
Y = np.array([[5.0, 6.0], [7.0, 8.0]])

tich_ma_tran = X @ Y
# [[19. 22.]
#  [43. 50.]]

6.2. Các hàm trong mô-đun numpy.linalg ​

  • numpy.linalg.inv(A): Tính ma trận nghịch đảo của ma trận vuông A.
  • numpy.linalg.solve(A, b): Giải hệ phương trình đại số tuyến tính Ax=b mà không cần tính ma trận nghịch đảo rõ ràng, giúp tăng độ chính xác số trị và tiết kiệm bộ nhớ.
  • numpy.linalg.qr(A): Phân rã ma trận thành tích QR (với Q là ma trận trực giao và R là ma trận tam giác trên).
  • numpy.linalg.svd(A): Phân rã giá trị suy biến U,Σ,VT.
  • numpy.linalg.det(A): Tính định thức của ma trận vuông.
  • numpy.linalg.eig(A): Tìm trị riêng và vector riêng.

7. Nghiên cứu tình huống: Mô phỏng bước đi ngẫu nhiên ​

Một bước đi ngẫu nhiên đơn giản xuất phát từ vị trí 0, tại mỗi bước di chuyển sang trái (−1) hoặc sang phải (+1) với xác suất ngang nhau p=0,5.

7.1. Mô phỏng một bước đi ngẫu nhiên ​

So sánh giữa cách viết vòng lặp Python thuần và cách viết vector hóa trong NumPy:

python
# Cách viết vector hóa trong NumPy:
n_buoc = 1000
rng = np.random.default_rng(seed=42)
buoc = np.where(rng.integers(0, 2, size=n_buoc) > 0, 1, -1)
vi_tri = np.cumsum(buoc)

print(f"Vị trí nhỏ nhất: {vi_tri.min()}, Vị trí lớn nhất: {vi_tri.max()}")

7.2. Mô phỏng đồng thời 5.000 bước đi ngẫu nhiên ​

Thay vì dùng vòng lặp mô phỏng từng bước đi một, NumPy cho phép mô phỏng đồng thời 5000 bước đi độc lập, mỗi bước đi kéo dài 1000 bước, chỉ bằng một mảng 2 chiều:

python
n_luot = 5000
n_buoc = 1000

# Sinh đồng thời 5.000 x 1.000 bước đi
buoc_nhieu = np.where(rng.integers(0, 2, size=(n_luot, n_buoc)) > 0, 1, -1)
buoc_di = np.cumsum(buoc_nhieu, axis=1)

print(f"Kích thước mảng mô phỏng: {buoc_di.shape}")  # (5000, 1000)

Ta có thể tính toán thời điểm đầu tiên mỗi bước đi chạm tới khoảng cách cách gốc ít nhất 30 bước (vượt ngưỡng +30 hoặc −30):

python
vuot_nguong_30 = np.abs(buoc_di) >= 30
co_vuot_nguong = vuot_nguong_30.any(axis=1)

print(f"Số lượng bước đi chạm ngưỡng 30: {co_vuot_nguong.sum()}")

# argmax dọc theo axis=1 trả về chỉ số đầu tiên gặp giá trị True
thoi_diem_cham = vuot_nguong_30[co_vuot_nguong].argmax(axis=1)
print(f"Thời gian chạm ngưỡng trung bình: {thoi_diem_cham.mean():.1f} bước")

Tài liệu tham khảo ​

8. Bài tập thực hành ​

Bài tập 1: Thời điểm đổi kiểu dữ liệu và tràn số nguyên

Cần nhân đôi các số trong mảng sau:

python
x = np.array([50, 100, 120], dtype=np.int8)
a = (x * 2).astype(np.int16)
b = x.astype(np.int16) * 2
  1. Dự đoán giá trị của mảng a và mảng b. Cách nào cho kết quả đúng với phép tính số học kỳ vọng?
  2. Tính dung lượng bộ nhớ nbytes của x, a, và b.
  3. Giải thích vì sao việc chuyển đổi sang kiểu dữ liệu int16 ở mảng a vẫn cho kết quả sai lệch.
Lời giải
  1. Dự đoán giá trị:
    • a: [100, -56, -16]
    • b: [100, 200, 240]
    • Cách b cho kết quả đúng với phép tính số học kỳ vọng.
  2. Dung lượng bộ nhớ:
    • Kiểu int8 chiếm 1 byte mỗi phần tử. Mảng x có 3 phần tử nên x.nbytes = 3 * 1 = 3 byte.
    • Kiểu int16 chiếm 2 byte mỗi phần tử. Mảng a và b có 3 phần tử nên a.nbytes = b.nbytes = 3 * 2 = 6 byte.
  3. Giải thích nguyên nhân sai lệch:
    • Ở biểu thức a = (x * 2).astype(np.int16), phép nhân x * 2 được thực hiện trước trên kiểu dữ liệu gốc int8 (miền giá trị từ −128 đến 127). Khi nhân 100×2=200 và 120×2=240, các giá trị này vượt quá ngưỡng 127 nên bị tràn số nhị phân thành 200−256=−56 và 240−256=−16. Phương thức .astype(np.int16) thực hiện sau đó chỉ làm nhiệm vụ chuyển đổi các giá trị đã bị sai lệch sang int16.
    • Ở biểu thức b, mảng x được chuyển đổi sang kiểu int16 trước (miền giá trị từ −32768 đến 32767), sau đó phép nhân đôi diễn ra an toàn mà không bị tràn số. Bài học: Luôn nâng cấp kiểu dữ liệu trước khi thực hiện các phép tính có nguy cơ vượt ngưỡng.

Bài tập 2: Thay thế vòng lặp for bằng phép toán mảng vector hóa

Đoạn mã Python sau tính bình phương chênh lệch của từng cặp số:

python
x = np.array([10, 12, 11, 20])
y = np.array([12, 11, 15, 20])
ket_qua = []
for u, v in zip(x, y):
    ket_qua.append((v - u) ** 2)
  1. Tính toán giá trị của danh sách ket_qua.
  2. Viết lại phép tính trên bằng một biểu thức NumPy duy nhất, không sử dụng vòng lặp for.
  3. Bản viết lại vẫn phải tính toán trên từng cặp số. Vì sao đối với các mảng dữ liệu lớn, biểu thức NumPy lại có tốc độ thực thi vượt trội hơn rất nhiều so với vòng lặp Python?
Lời giải
  1. Tính toán thủ công:
    • Cặp (10,12): (12−10)2=22=4.
    • Cặp (12,11): (11−12)2=(−1)2=1.
    • Cặp (11,15): (15−11)2=42=16.
    • Cặp (20,20): (20−20)2=02=0.
    • Kết quả: [4, 1, 16, 0].
  2. Viết lại bằng biểu thức NumPy:
python
ket_qua_np = (y - x) ** 2
  1. Giải thích nguyên nhân chênh lệch tốc độ:
    • Vòng lặp for của Python phải thông dịch từng lệnh bytecode ở mỗi vòng lặp, kiểm tra kiểu động của từng đối tượng và cấp phát các đối tượng số nguyên Python bọc ngoài (boxed objects).
    • Biểu thức (y - x) ** 2 gọi các phép toán mảng đã biên dịch. Nó vẫn xử lý từng cặp giá trị, nhưng giảm công việc gọi lệnh và xử lý đối tượng ở mỗi lượt Python. Truy cập liên tiếp có thể giúp sử dụng bộ nhớ đệm. Hiệu năng thực tế và việc dùng SIMD còn phụ thuộc dtype, bố trí mảng, phép toán và phần cứng.

Bài tập 3: Xác định vị trí phần tử trong bộ nhớ từ Strides

Cho mảng hai chiều A gồm các số nguyên 64-bit:

python
A = np.array([[10, 12, 11],
              [20, 21, 24],
              [30, 33, 31],
              [40, 44, 42]], dtype=np.int64)

Mảng A có kích thước shape = (4, 3) và bước nhảy strides = (24, 8) (đơn vị byte).

  1. Phần tử A[1,2] cách vị trí bắt đầu vùng dữ liệu của A bao nhiêu byte?
  2. Một phần tử nằm cách vị trí bắt đầu vùng dữ liệu 80 byte mang giá trị nào và tương ứng với chỉ số hàng, cột nào?
  3. Nếu ta chuyển đổi mảng sang kiểu số nguyên 32-bit: B = A.astype(np.int32). Kích thước shape và bước nhảy strides của mảng B bằng bao nhiêu? Giải thích vì sao shape không đổi nhưng strides lại thay đổi.
Lời giải
  1. Gọi O là độ lệch byte, s0 là bước nhảy hàng và s1 là bước nhảy cột. Với chỉ số hàng i và cột j:
    O=is0+js1
    Với phần tử A[1,2]:
    O=1⋅24+2⋅8,=40byte.
  2. Tìm chỉ số từ độ lệch 80 byte:
    • Chỉ số hàng i=80//24=3 (vì 3×24=72, phần dư là 8).
    • Chỉ số cột j=(80−72)//8=8//8=1.
    • Phần tử tương ứng là A[3,1], có giá trị bằng 44.
  3. Khi chuyển sang int32:
    • Kích thước của mỗi phần tử giảm từ 8 byte xuống 4 byte (itemsize = 4).
    • shape vẫn giữ nguyên là (4, 3) vì cấu trúc số hàng và số cột không đổi.
    • Bước nhảy strides thay đổi: Bước cột bằng kích thước một phần tử là 4 byte, trong khi bước hàng bằng 3 cột ×4 byte =12 byte. Như vậy, B.strides = (12, 4).

Bài tập 4: Xác định các ô của View và kiểm chứng bộ nhớ dùng chung

Cho mảng A tương tự bài trước:

python
A = np.array([[10, 12, 11],
              [20, 21, 24],
              [30, 33, 31],
              [40, 44, 42]], dtype=np.int64)

V = A[::2, 1:]
  1. Xác định các giá trị và kích thước V.shape của mảng V.
  2. Tính bước nhảy V.strides. Phần tử đầu tiên của V cách vị trí bắt đầu vùng dữ liệu của A bao nhiêu byte?
  3. Nếu thực hiện phép gán V[1, 0] = 999, phần tử nào của mảng A sẽ bị thay đổi? Giải thích bằng vị trí trong bộ nhớ.
Lời giải
  1. Lát cắt ::2 lấy các hàng với bước nhảy 2, tức là chọn hàng 0 và hàng 2. Lát cắt 1: lấy các cột từ chỉ số 1 trở đi, tức là chọn cột 1 và cột 2.
    • Hàng 0 của V là hàng 0 của A lấy cột 1, 2: [12, 11].
    • Hàng 1 của V là hàng 2 của A lấy cột 1, 2: [33, 31].
    • Giá trị của V: [[12, 11], [33, 31]].
    • Kích thước: V.shape = (2, 2).
  2. Bước nhảy V.strides:
    • Để nhảy từ hàng 0 sang hàng 1 của V, trong mảng gốc A ta phải nhảy qua 2 hàng. Mỗi hàng của A tốn 24 byte, nên bước nhảy hàng của V là 2×24=48 byte.
    • Bước nhảy cột của V vẫn là khoảng cách giữa hai cột liên tiếp, tương ứng với 8 byte.
    • Vậy V.strides = (48, 8).
    • Phần tử đầu tiên của V chính là A[0,1]. Độ lệch byte so với đầu mảng A bằng 0×24+1×8=8 byte.
  3. Phần tử V[1,0] tương ứng với hàng 1, cột 0 của V:
    • Hàng 1 của V tương ứng với hàng 2 của A.
    • Cột 0 của V tương ứng với cột 1 của A.
    • Do đó, V[1,0] ánh xạ trực tiếp tới phần tử A[2,1]. Khi gán V[1, 0] = 999, giá trị 33 của A[2,1] sẽ bị ghi đè thành 999.

Bài tập 6: Sửa lỗi phép trừ trung bình hàng do hiểu sai Broadcasting

Một bạn sinh viên muốn chuẩn hóa mảng dữ liệu M bằng cách lấy mỗi phần tử trừ đi giá trị trung bình của hàng chứa nó:

python
M = np.array([[10, 12, 11],
              [20, 21, 24],
              [30, 33, 31]])

tb = M.mean(axis=1)
chenh_lech = M - tb

Ví dụ: Hàng đầu tiên [10,12,11] có trung bình là 11, kết quả mong muốn là [−1,1,0].

  1. Chạy thử đoạn mã trên cho thấy chương trình không báo lỗi nhưng kết quả lại tính sai. Hãy giải thích nguyên nhân gây ra lỗi logic này.
  2. Hãy sửa lại đoạn mã để phép tính cho kết quả đúng và kiểm chứng giá trị của hàng đầu tiên.
Lời giải
  1. Nguyên nhân lỗi logic:
    • Mảng M có kích thước shape = (3, 3).
    • Lệnh tb = M.mean(axis=1) tính trung bình theo hàng, nhưng trả về một mảng một chiều có shape = (3,).
    • Theo quy tắc phát sóng của NumPy, khi thực hiện M - tb, mảng 1 chiều tb được căn chỉnh theo trục cuối cùng (trục cột), tức là tb được coi như một hàng (1,3) và kéo dài dọc theo 3 hàng. Kết quả là mỗi cột của M bị trừ đi phần tử tương ứng của tb, hoàn toàn trái ngược với ý muốn ban đầu là trừ theo từng hàng.
  2. Sửa lại đoạn mã: Cần giữ nguyên số chiều của mảng kết quả trung bình bằng tham số keepdims=True để tb có shape = (3, 1). Khi đó NumPy sẽ kéo dài cột trung bình ngang qua 3 cột của mỗi hàng:
python
tb_chuan = M.mean(axis=1, keepdims=True)  # shape = (3, 1)
chenh_lech = M - tb_chuan

Kiểm tra hàng đầu tiên:

  • Trung bình hàng: (10+12+11)/3=11.
  • Chênh lệch: [10−11,12−11,11−11]=[−1,1,0]. Phép tính hoàn toàn chính xác.

Lab 03: NumPy và tư duy vector hóa trong xử lý dữ liệu ​

Lấy một phần của mảng có thể tạo view hoặc bản sao. Sự khác biệt xuất hiện rõ khi sửa kết quả và quan sát mảng ban đầu. Các bài dùng mảng nhỏ để kiểm tra từng phần tử, bước nhảy bộ nhớ, quy tắc broadcasting và cách lấy mẫu.

Ma trận và các bảng giá trong bộ bài là dữ liệu giả lập. Đơn vị giá của từng bảng được ghi ngay trong đề.

Khởi tạo dữ liệu dùng chung ​

Mọi bài tập sử dụng chung ma trận A kích thước 4 hàng × 3 cột với kiểu số nguyên 64-bit (np.int64):

python
import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

Khi hiển thị trực quan dưới dạng bảng toán học:

A=[101211202124303331404442]

Bài 1: Đọc cấu trúc mảng, bước nhảy bộ nhớ và khoảng cách byte ​

Mục tiêu: Nắm vững cấu trúc mô tả siêu dữ liệu (metadata) của đối tượng mảng ndarray, hiểu rõ quy tắc sắp xếp dữ liệu theo thứ tự hàng (C-contiguous) trong bộ nhớ và cơ chế tính toán độ lệch địa chỉ (byte offset).

Dữ liệu đầu vào: Ma trận A kích thước 4×3, kiểu np.int64.

Thao tác yêu cầu:

  1. Đọc và xác định bốn thuộc tính cấu trúc cơ bản của A: Kích thước (shape), kích thước mỗi phần tử tính theo byte (itemsize), số byte của các phần tử logic trong mảng (nbytes), và bước nhảy bộ nhớ (strides).
  2. Tính khoảng cách địa chỉ tính theo byte (offset) từ vị trí bắt đầu của phần tử đầu tiên A[0,0] đến phần tử A[3,1] dựa vào chỉ số hàng, chỉ số cột và bước nhảy strides.
  3. Chuyển đổi ma trận A sang kiểu số nguyên 32-bit (np.int32) tạo thành mảng mới A32. So sánh dung lượng dữ liệu (nbytes) và bước nhảy (strides) của A32 so với A.
  4. Đóng gói quy trình trên thành một hàm tái sử dụng array_layout(matrix: np.ndarray, row: int, col: int) -> dict tuân thủ đúng quy chuẩn cấu trúc dữ liệu, hỗ trợ ma trận hai chiều bất kỳ.

Trường hợp biên cần chú ý:

  • Chỉ số row và col phải nằm trong phạm vi hợp lệ của ma trận (0≤row<nrows và 0≤col<ncols).
  • Không ghi cứng các hằng số kích thước của A vào thân hàm.
Lời giải

Cách 1: Thao tác tuần tự trực tiếp trên thuộc tính đối tượng ndarray ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np

# 1. Truy xuất các thuộc tính cấu trúc của ma trận A
shape_a = A.shape
byte_moi_so = A.itemsize
byte_du_lieu = A.nbytes
buoc = A.strides

# 2. Tính độ lệch byte của phần tử A[3, 1] từ bước nhảy
hang_can_tim = 3
cot_can_tim = 1
offset = hang_can_tim * buoc[0] + cot_can_tim * buoc[1]

# 3. Tạo mảng với kiểu dữ liệu int32
A32 = A.astype(np.int32)
byte_du_lieu_32 = A32.nbytes
buoc_32 = A32.strides

# Kết thúc lời giải

print(
    {
        "shape": shape_a,
        "itemsize": byte_moi_so,
        "nbytes": byte_du_lieu,
        "strides": buoc,
        "offset": offset,
        "int32_nbytes": byte_du_lieu_32,
        "int32_strides": buoc_32,
    }
)

Giải thích cách 1 ​

shape ghi số phần tử theo từng trục, ở đây là bốn hàng và ba cột. itemsize cho biết mỗi số int64 cần tám byte. Có mười hai phần tử nên nbytes bằng 96. Đây là số byte của các phần tử logic, không bao gồm toàn bộ bộ nhớ dành cho đối tượng Python và thông tin quản lý mảng.

Ma trận được khởi tạo liền theo hàng, nên sang cột kế tiếp cần tám byte và sang hàng kế tiếp cần ba lần tám, tức hai mươi bốn byte. strides vì thế là (24, 8). Độ lệch của ô [3, 1] bằng ba bước hàng cộng một bước cột, tức 80 byte. astype(np.int32) tạo mảng kiểu mới có phần tử bốn byte, nên dung lượng phần tử giảm còn 48 byte. Các giá trị nhỏ trong mẫu chuyển kiểu mà không đổi giá trị.

Kết quả cách 1 ​

Thuộc tínhA (int64)A32 (int32)
shape(4, 3)(4, 3)
itemsize8 byte4 byte
nbytes96 byte48 byte
strides(24, 8)(12, 4)

Biến offset bằng 80 byte, tính từ vị trí phần tử A[0, 0].

Cách 2: Đóng gói hàm chuẩn hóa array_layout với kiểm soát ràng buộc dữ liệu ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np


def array_layout(matrix: np.ndarray, row: int, col: int) -> dict:
    """Đọc cấu trúc bộ nhớ và tính toán độ lệch byte của phần tử chỉ định.

    Tham số:
        matrix: Mảng hai chiều kiểu số nguyên, định dạng bộ nhớ C-contiguous.
        row: Chỉ số hàng hợp lệ, không âm.
        col: Chỉ số cột hợp lệ, không âm.

    Trả về:
        Từ điển chứa shape, itemsize, nbytes, strides, offset và mảng int32.
    """
    if matrix.ndim != 2:
        raise ValueError("Yêu cầu ma trận đầu vào phải có đúng 2 chiều.")
    if not (0 <= row < matrix.shape[0] and 0 <= col < matrix.shape[1]):
        raise IndexError(
            f"Chỉ số ({row}, {col}) vượt quá kích thước ma trận {matrix.shape}."
        )

    # Tính toán độ lệch byte trực tiếp từ bước nhảy
    strides = matrix.strides
    offset = int(row * strides[0] + col * strides[1])

    return {
        "shape": matrix.shape,
        "itemsize": matrix.itemsize,
        "nbytes": matrix.nbytes,
        "strides": strides,
        "offset": offset,
        "int32_array": matrix.astype(np.int32),
    }


print(array_layout(A, 3, 1))

# Kết thúc lời giải

Giải thích cách 2 ​

array_layout nhận ma trận cùng chỉ số hàng, cột cần khảo sát và trả về một từ điển. Kiểm tra matrix.ndim != 2 từ chối đầu vào không phải bảng hai trục. Điều kiện tiếp theo yêu cầu chỉ số không âm và nhỏ hơn số phần tử ở trục tương ứng, nên chặn truy cập vượt phạm vi trước khi tính địa chỉ.

Hàm đọc trực tiếp matrix.strides rồi tính row * strides[0] + col * strides[1]. Không ghi cứng số cột hay số byte, vì view hoặc kiểu dữ liệu khác có bước nhảy khác. Mảng int32_array được tạo bởi astype, còn các trường còn lại mô tả mảng ban đầu. Chuyển sang số nguyên hẹp hơn chỉ bảo toàn giá trị khi các giá trị nằm trong miền biểu diễn của kiểu mới.

Kết quả cách 2 ​

Thuộc tínhA (int64)A32 (int32)
shape(4, 3)(4, 3)
itemsize8 byte4 byte
nbytes96 byte48 byte
strides(24, 8)(12, 4)

Trường offset trong từ điển bằng 80. Trường int32_array giữ cùng bốn hàng giá trị của A với dtype=int32.

So sánh hai cách ​

Cách 1 phơi bày từng phép đọc thuộc tính để đối chiếu bằng tay. Cách 2 nhận chỉ số qua tham số và bổ sung kiểm tra phạm vi. Cả hai đọc cùng thông tin, không phải đo mức sử dụng bộ nhớ của toàn bộ tiến trình.


Bài 2: Lát cắt mảng đa chiều và tính chất view chia sẻ bộ nhớ ​

Mục tiêu: Nắm vững cú pháp lát cắt bước nhảy (strided slicing) trên mảng hai chiều, phân biệt cách quản lý vùng nhớ thông qua cơ chế View, và tính toán tọa độ ánh xạ ngược về mảng gốc.

Dữ liệu đầu vào: Ma trận A kích thước 4×3, kiểu np.int64.

Thao tác yêu cầu:

  1. Tạo ma trận con V gồm các hàng chỉ mục 1, 3 và các cột chỉ mục 0, 2 của A, bắt buộc sử dụng duy nhất một lát cắt cho mỗi chiều không gian.
  2. Dự đoán giá trị các phần tử, kích thước (shape), và bước nhảy (strides) của V.
  3. Xác định phần tử V[1,1] tương ứng với ô nào trong ma trận gốc A, và tính khoảng cách byte từ đầu vùng đệm dữ liệu của A đến ô này.
  4. Kiểm tra xem V có thực sự dùng chung vùng nhớ với A hay không bằng hàm np.shares_memory(A, V) và kiểm tra thuộc tính con trỏ dữ liệu gốc (base).
  5. Đóng gói thành hàm slice_view(matrix: np.ndarray) -> dict tuân thủ đúng quy chuẩn dữ liệu.

Trường hợp biên cần chú ý:

  • Không được sửa đổi giá trị các phần tử trong V.
  • Ma trận đầu vào cần có ít nhất 4 hàng và 3 cột để lát cắt bước nhảy hoạt động ổn định.
Lời giải

Cách 1: Sử dụng cú pháp lát cắt bước nhảy và truy xuất trực tiếp ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np

# 1. Tạo lát cắt V với một cú pháp slice cho mỗi chiều
V = A[1::2, 0::2]

# 2. Thu thập siêu dữ liệu của V
shape_v = V.shape
strides_v = V.strides

# 3. Tính độ lệch từ đầu mảng A đến ô V[1, 1] (tức A[3, 2])
hang_goc = 1 + 1 * 2  # Chỉ số 3
cot_goc = 0 + 1 * 2  # Chỉ số 2
offset_v11 = hang_goc * A.strides[0] + cot_goc * A.strides[1]

# 4. Kiểm tra chia sẻ vùng nhớ chung
dung_chung = np.shares_memory(A, V)
dung_chung_base = V.base is A

# Kết thúc lời giải

print("View:", V)
print("Offset:", offset_v11)
print("Shared memory:", dung_chung)

Giải thích cách 1 ​

Lát cắt 1::2 chọn các hàng bắt đầu từ chỉ số 1, mỗi lần tăng 2, nên lấy hàng 1 và 3. Lát cắt 0::2 tương tự lấy cột 0 và 2. Kết quả V có hai hàng, hai cột và chứa các giá trị của đúng bốn giao điểm này.

Lát cắt cơ bản tạo view, nên bước hàng của V là hai lần bước hàng A, tức 48 byte. Bước cột là hai lần tám, tức 16 byte. Ô V[1, 1] quy ngược về A[3, 2], cách phần tử đầu A một khoảng 88 byte. np.shares_memory(A, V) kiểm tra thực sự có vùng nhớ dùng chung. Trên A sở hữu dữ liệu của mẫu, V.base is A cũng là True, nhưng kiểm tra base không đủ cho mọi chuỗi view.

Kết quả cách 1 ​

Hàng của VCột 0Cột 1
02024
14042
Thuộc tínhKết quả
shape_v(2, 2)
strides_v(48, 16)
offset_v1188 byte từ A[0, 0]
dung_chungTrue
dung_chung_baseTrue trên A của mẫu

Cách 2: Đóng gói hàm slice_view kiểm soát ma trận tổng quát ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np


def slice_view(matrix: np.ndarray) -> dict:
    """Trích xuất lát cắt bước nhảy và phân tích siêu dữ liệu chia sẻ bộ nhớ.

    Tham số:
        matrix: Mảng hai chiều kiểu số nguyên, kích thước tối thiểu 4 hàng và 3 cột.

    Trả về:
        Từ điển chứa view, shape, strides, offset_v11 và shares_memory.
    """
    if matrix.ndim != 2:
        raise ValueError("Yêu cầu ma trận đầu vào phải có 2 chiều.")
    if matrix.shape[0] < 4 or matrix.shape[1] < 3:
        raise ValueError(
            f"Kích thước ma trận {matrix.shape} không đủ để cắt hàng [1, 3] và cột [0, 2]."
        )

    # Tạo lát cắt bước nhảy hai chiều
    view = matrix[1::2, 0::2]

    # Tính tọa độ thực tế của view[1, 1] trên ma trận gốc
    orig_row = 1 + 1 * 2
    orig_col = 0 + 1 * 2
    offset_v11 = orig_row * matrix.strides[0] + orig_col * matrix.strides[1]

    return {
        "view": view,
        "shape": view.shape,
        "strides": view.strides,
        "offset_v11": offset_v11,
        "shares_memory": bool(np.shares_memory(matrix, view)),
    }


print(slice_view(A))

# Kết thúc lời giải

Giải thích cách 2 ​

slice_view yêu cầu đầu vào có hai trục, ít nhất bốn hàng và ba cột. Những điều kiện này bảo đảm vị trí view[1, 1] tồn tại. Phép cắt vẫn dùng các điểm bắt đầu và bước nhảy cố định của đề, còn độ lệch được tính từ bước nhảy thực tế của mảng truyền vào.

Từ điển trả về gồm chính view và các thuộc tính để kiểm chứng. bool(np.shares_memory(matrix, view)) chuyển kết quả kiểm tra thành Boolean Python. Hàm không gán giá trị vào view. Nếu nơi gọi sửa một phần tử của view, giá trị tại ô tương ứng trong mảng gốc có thể thay đổi vì chúng chia sẻ dữ liệu.

Kết quả cách 2 ​

Hàng của VCột 0Cột 1
02024
14042
Trường kết quảGiá trị
shape(2, 2)
strides(48, 16)
offset_v1188
shares_memoryTrue

So sánh hai cách ​

Cách 1 minh họa quan hệ giữa chỉ số và vùng nhớ. Cách 2 đóng gói phép khảo sát với điều kiện đầu vào rõ ràng. Cả hai tạo view, nên không dành một bản sao riêng cho bốn phần tử được chọn.


Bài 3: Phân biệt view và bản sao độc lập khi đột biến dữ liệu ​

Mục tiêu: Hiểu rõ ranh giới kỹ thuật giữa phép lập chỉ mục cơ bản (Basic Slicing tạo View) và phép lập chỉ mục nâng cao (Advanced Fancy Indexing tạo Copy), kiểm soát hoàn toàn tác dụng phụ khi gán giá trị (mutation side-effects).

Dữ liệu đầu vào: Bản sao độc lập B=A.copy() của ma trận A.

Thao tác yêu cầu:

  1. Khảo sát hiện tượng đột biến dữ liệu:
    • Tạo V=B[:,1:3] (sử dụng lát cắt slicing).
    • Tạo C=B[:,[1,2]] (sử dụng danh sách chỉ mục fancy indexing).
    • Dự đoán và quan sát giá trị hàng đầu tiên B[0] sau lệnh gán C[0,0]=−9.
    • Dự đoán và quan sát giá trị hàng đầu tiên B[0] sau lệnh gán tiếp theo V[0,0]=−1.
    • Giải thích bản chất tại sao hai thao tác trên lại mang lại hành vi khác nhau hoàn toàn.
  2. Xây dựng phương pháp trích xuất mảng D chứa cột 1 và 2 của B sao cho mọi thao tác chỉnh sửa trên D hoàn toàn độc lập và không làm biến đổi mảng gốc B.
  3. Đóng gói hàm copy_columns(matrix: np.ndarray) -> np.ndarray bảo đảm tính độc lập bộ nhớ tuyệt đối.

Trường hợp biên cần chú ý:

  • Mảng đầu vào phải có ít nhất 1 hàng và 3 cột.
  • Kiểm tra tính độc lập bộ nhớ bằng not np.shares_memory(result, matrix).
Lời giải

Cách 1: Thực nghiệm gán giá trị và sao chép tường minh qua copy() ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np

# Khởi tạo bản sao độc lập của A
B = A.copy()

# Tạo View và Fancy Indexing Copy
V = B[:, 1:3]  # View
C = B[:, [1, 2]]  # Copy

# Thao tác 1: Gán trên bản sao C
C[0, 0] = -9
# Kết quả B[0] vẫn giữ nguyên là [10, 12, 11]

# Thao tác 2: Gán trên View V
V[0, 0] = -1
# Kết quả B[0] bị thay đổi thành [10, -1, 11]

# Tạo mảng D độc lập an toàn chứa cột 1 và 2
D = B[:, 1:3].copy()
D[0, 0] = 999
# Giá trị của B hoàn toàn không bị ảnh hưởng bởi D

# Kết thúc lời giải

print("Original:", A)
print("View parent:", B)
print("Fancy-index copy:", C)
print("Explicit copy:", D)

Giải thích cách 1 ​

B = A.copy() tạo vùng dữ liệu riêng trước khi làm thí nghiệm, nên A giữ nguyên. V = B[:, 1:3] là lát cắt cơ bản dùng chung dữ liệu với B. C = B[:, [1, 2]] dùng danh sách chỉ mục nâng cao, tạo một bản sao các cột được chọn.

Gán C[0, 0] = -9 chỉ sửa bản sao C, nên hàng đầu B còn [10, 12, 11]. Gán tiếp V[0, 0] = -1 sửa cùng ô dữ liệu với B[0, 1], khiến hàng đầu B thành [10, -1, 11]. Cuối cùng .copy() trên lát cắt tạo D độc lập. Gán D[0, 0] = 999 không truyền sang B.

Kết quả cách 1 ​

Thời điểmB[0]Giá trị vừa gán
Sau khi gán C[10, 12, 11]C[0, 0] = -9
Sau khi gán V[10, -1, 11]V[0, 0] = -1
Sau khi gán D[10, -1, 11]D[0, 0] = 999

Cách 2: Đóng gói hàm copy_columns bảo đảm độc lập bộ nhớ tuyệt đối ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np


def copy_columns(matrix: np.ndarray) -> np.ndarray:
    """Trích xuất cột 1 và 2 của ma trận dưới dạng bản sao bộ nhớ độc lập.

    Tham số:
        matrix: Mảng hai chiều kiểu số bất kỳ, có ít nhất một hàng và ba cột.

    Trả về:
        Mảng con hai chiều gồm cột 1 và 2, hoàn toàn không chia sẻ vùng nhớ với mảng gốc.
    """
    if matrix.ndim != 2:
        raise ValueError("Yêu cầu ma trận đầu vào phải có đúng 2 chiều.")
    if matrix.shape[1] < 3:
        raise ValueError(
            f"Ma trận cần có ít nhất 3 cột, hiện tại có {matrix.shape[1]} cột."
        )

    # Trích xuất lát cắt và thực hiện nhân bản vùng đệm dữ liệu tường minh
    result = matrix[:, 1:3].copy()

    # Xác minh tính toàn vẹn bộ nhớ
    if np.shares_memory(result, matrix):
        raise RuntimeError(
            "Lỗi kỹ thuật: Kết quả vẫn đang chia sẻ bộ nhớ với mảng gốc."
        )

    return result


print(copy_columns(A))

# Kết thúc lời giải

Giải thích cách 2 ​

copy_columns kiểm tra số trục và số cột, sau đó lấy cột 1, 2 bằng lát cắt rồi gọi .copy() một cách tường minh. Giá trị trả về là một ndarray mới với cùng số hàng và hai cột. Không có phép gán nào vào matrix trong hàm.

np.shares_memory(result, matrix) được dùng như phép kiểm chứng tính độc lập vùng dữ liệu. Nếu còn chia sẻ, hàm phát sinh RuntimeError thay vì trả một kết quả trái yêu cầu. Khi truyền A nguyên vẹn, các giá trị trả về là bốn cặp cột ở bảng dưới. Việc sao chép có chi phí bộ nhớ tương ứng với số phần tử được chọn, đổi lại cho phép sửa kết quả độc lập.

Kết quả cách 2 ​

Hàng kết quảCột 0 (A cột 1)Cột 1 (A cột 2)
01211
12124
23331
34442

np.shares_memory(copy_columns(A), A) cho False.

So sánh hai cách ​

Cách 1 dùng các lần gán để chứng minh tác động của view và copy. Cách 2 đáp ứng trực tiếp nhu cầu lấy hai cột độc lập để xử lý tiếp. Việc hai mảng có cùng giá trị không cho biết chúng có dùng chung vùng dữ liệu hay không.


Bài 4: Chọn phần tử nâng cao: Boolean mask, fancy indexing và lưới trực giao np.ix_ ​

Mục tiêu: Làm chủ ba kỹ thuật trích xuất phần tử nâng cao trong NumPy mà không dùng vòng lặp for: Mặt nạ logic Boolean, Cặp mảng chỉ mục tọa độ (Coordinate Fancy Indexing), và Lưới chỉ mục trực giao np.ix_. Giải thích bản chất sự khác biệt về kích thước (shape) của các kết quả.

Dữ liệu đầu vào: Ma trận A kích thước 4×3, kiểu np.int64.

Thao tác yêu cầu:

  1. Trích xuất toàn bộ các phần tử là số chẵn trong A theo thứ tự duyệt từng hàng (row-major order) vào mảng so_chan.
  2. Trích xuất đồng thời hai phần tử rời rạc A[0,1] và A[2,2] bằng cặp mảng chỉ mục vào mảng hai_o.
  3. Trích xuất ma trận con kích thước 2×2 gồm cột 1, 2 tại các hàng 0, 2 bằng hàm tạo lưới trực giao np.ix_ vào mảng bon_o.
  4. Ghi lại kích thước (shape) của cả ba kết quả và giải thích cặn kẽ bản chất toán học tại sao chúng lại có số chiều và hình dạng khác nhau.
  5. Đóng gói thành hàm select_elements(matrix: np.ndarray) -> dict tuân thủ đúng quy chuẩn dữ liệu.

Trường hợp biên cần chú ý:

  • Không sử dụng bất kỳ vòng lặp for hay biểu thức comprehension nào.
  • Các kết quả trích xuất phải giữ nguyên kiểu dữ liệu và không chia sẻ bộ nhớ với ma trận đầu vào.
Lời giải

Cách 1: Áp dụng cú pháp trực tiếp của NumPy theo từng yêu cầu trích xuất ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np

# 1. Trích xuất các số chẵn theo thứ tự hàng bằng Boolean Mask
mat_na_chan = A % 2 == 0
so_chan = A[mat_na_chan]

# 2. Trích xuất hai ô rời rạc bằng cặp mảng chỉ mục tọa độ
hang_chon = [0, 2]
cot_chon = [1, 2]
hai_o = A[hang_chon, cot_chon]

# 3. Trích xuất bốn ô giao điểm bằng lưới trực giao np.ix_
bon_o = A[np.ix_([0, 2], [1, 2])]

# Kết thúc lời giải

print("Paired selection:", hai_o)
print("Cartesian selection:", bon_o)

Giải thích cách 1 ​

A % 2 == 0 tạo mặt nạ Boolean cùng shape với A. Mỗi ô đúng tương ứng một số chẵn. A[mat_na_chan] gom các ô đúng theo thứ tự hàng vào mảng một trục, nên tám số chẵn có shape (8,).

Với hai danh sách chỉ mục bằng nhau về độ dài, A[hang_chon, cot_chon] ghép phần tử theo từng vị trí: Cặp thứ nhất là (0, 1), cặp thứ hai là (2, 2). Kết quả chỉ có hai số 12, 31. Ngược lại, np.ix_([0, 2], [1, 2]) dựng chỉ mục để lấy mọi giao điểm của hai hàng và hai cột, cho bảng hai hàng hai cột.

Kết quả cách 1 ​

Kết quảGiá trịshape
so_chan[10, 12, 20, 24, 30, 40, 44, 42](8,)
hai_o[12, 31](2,)
bon_o, hàng 0[12, 11](2, 2) cho toàn bảng
bon_o, hàng 1[33, 31]

Cách 2: Đóng gói hàm select_elements kết hợp kỹ thuật broadcasting tọa độ thay thế np.ix_ ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np


def select_elements(matrix: np.ndarray) -> dict:
    """Trích xuất phần tử theo ba cơ chế: Boolean mask, fancy indexing và lưới tọa độ.

    Tham số:
        matrix: Mảng hai chiều kiểu số nguyên, kích thước tối thiểu 3 hàng và 3 cột.

    Trả về:
        Từ điển chứa so_chan (1D), hai_o (1D) và bon_o (2D).
    """
    if matrix.ndim != 2:
        raise ValueError("Yêu cầu ma trận đầu vào phải có 2 chiều.")
    if matrix.shape[0] < 3 or matrix.shape[1] < 3:
        raise ValueError("Ma trận cần có kích thước tối thiểu 3x3.")

    # 1. Lọc phần tử chẵn qua Boolean Mask
    so_chan = matrix[matrix % 2 == 0]

    # 2. Chọn hai phần tử tọa độ [0, 1] và [2, 2]
    hai_o = matrix[[0, 2], [1, 2]]

    # 3. Lấy giao điểm 4 ô bằng kỹ thuật broadcasting chỉ mục thay thế np.ix_
    rows = np.array([0, 2])[:, np.newaxis]  # shape (2, 1)
    cols = np.array([1, 2])[np.newaxis, :]  # shape (1, 2)
    bon_o = matrix[rows, cols]  # shape (2, 2)

    return {"so_chan": so_chan, "hai_o": hai_o, "bon_o": bon_o}


ket_qua = select_elements(A)
for ten, mang in ket_qua.items():
    print(ten, mang)

# Kết thúc lời giải

Giải thích cách 2 ​

select_elements kiểm tra đầu vào có hai trục và đủ ba hàng, ba cột cho các chỉ số cố định. Hai kết quả đầu dùng cùng mặt nạ và cặp chỉ mục như Cách 1. Với kết quả thứ ba, hàm tạo mảng hàng shape (2, 1) và mảng cột shape (1, 2) bằng np.newaxis.

Khi hai mảng chỉ mục được broadcasting, chúng tạo lưới tọa độ shape (2, 2), tương đương lưới của np.ix_. Cả ba cách lấy phần tử trong hàm đều thuộc lập chỉ mục nâng cao và cho bản sao dữ liệu. Vòng lặp cuối chỉ in các kết quả đã tính, không phải vòng lặp dùng để chọn từng phần tử của A.

Kết quả cách 2 ​

Kết quảGiá trịshape
so_chan[10, 12, 20, 24, 30, 40, 44, 42](8,)
hai_o[12, 31](2,)
bon_o, hàng 0[12, 11](2, 2) cho toàn bảng
bon_o, hàng 1[33, 31]

So sánh hai cách ​

Cách 1 dùng np.ix_ để biểu đạt trực tiếp giao của các hàng và cột. Cách 2 khai triển cơ chế lưới bằng cách thêm trục. Cần phân biệt chọn cặp tọa độ với chọn mọi giao điểm, vì chúng khác cả số phần tử lẫn shape.


Bài 5: Cơ chế phát sóng broadcasting và phép cộng theo chiều không gian ​

Mục tiêu: Nắm vững nguyên lý và thuật toán căn lề chiều (Trailing Dimensions Alignment) của cơ chế phát sóng Broadcasting trong NumPy, thực hiện các phép toán cộng trừ ma trận theo từng chiều mà không dùng vòng lặp.

Dữ liệu đầu vào:

  • Ma trận A kích thước 4×3, kiểu np.int64.
  • Vector cộng theo cột: b=np.array([1,2,3]).
  • Vector cộng theo hàng: d=np.array([10,20,30,40]).

Thao tác yêu cầu:

  1. Tạo ma trận K qua hai bước cộng vector hóa:
    • Bước 1: Từ A, cộng các giá trị 1,2,3 lần lượt vào các cột 0,1,2.
    • Bước 2: Trên kết quả vừa thu được, cộng các giá trị 10,20,30,40 lần lượt vào các hàng 0,1,2,3. Ví dụ: Ô K[1, 2] nhận giá trị gốc 24, phần cộng theo cột là 3 và phần cộng theo hàng là 20. Kết quả là 47. Hãy tính thêm cả hàng đầu tiên của K để đối chiếu với mã.
  2. Ghi rõ kích thước (shape) của từng mảng tham gia trong phép toán ở mỗi bước.
  3. Giải thích tại sao phép cộng trực tiếp A + d phát sinh lỗi ValueError, và chỉ rõ cách khắc phục.
  4. Đóng gói hàm broadcast_add(matrix: np.ndarray, column_add: np.ndarray, row_add: np.ndarray) -> np.ndarray áp dụng cho ma trận và các vector có kích thước bất kỳ.

Trường hợp biên cần chú ý:

  • Không làm thay đổi dữ liệu của ma trận gốc A.
  • Hỗ trợ cả số nguyên lẫn số thực.
  • Không giả định số hàng bằng số cột của ma trận.
Lời giải

Cách 1: Thực hiện phép cộng hai bước kết hợp biến đổi trục bằng reshape ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

# Bắt đầu lời giải

import numpy as np

b = np.array([1, 2, 3])
d = np.array([10, 20, 30, 40])

# Bước 1: Cộng vector b vào từng cột của A
# Shape của A là (4, 3), shape của b là (3,). Kết quả bước 1 có shape (4, 3).
K1 = A + b

# Bước 2: Chuyển d thành ma trận cột có shape (4, 1) và cộng theo hàng
d_cot = d.reshape(-1, 1)  # shape (4, 1)
K = K1 + d_cot  # shape (4, 3)
np.testing.assert_array_equal(K[0], [21, 24, 24])
print("Hàng đầu của K:", K[0])

# Kết thúc lời giải

Giải thích cách 1 ​

b có shape (3,), khớp số cột nên mỗi hàng của A được cộng cùng ba số 1, 2, 3. K1 vẫn có shape (4, 3). Vector d có bốn phần tử nhưng chưa có trục cột, nên phải đổi thành shape (4, 1) bằng reshape(-1, 1) trước khi cộng theo hàng.

Số -1 trong reshape yêu cầu NumPy suy ra độ dài trục đó từ số phần tử, còn 1 đặt một cột. Sau cộng, ô K[1, 2] bằng giá 24 cộng 3 rồi cộng 20, được 47. np.testing.assert_array_equal kiểm tra chính xác hàng đầu [21, 24, 24] và phát sinh lỗi nếu sai. Phép A + d không hợp lệ vì trục cuối có độ dài 3 và 4, không bằng nhau và không có trục nào bằng 1.

Kết quả cách 1 ​

HàngK, cột 0K, cột 1K, cột 2
0212424
1414347
2616564
3818685

Dòng in của đoạn mã là Hàng đầu của K: [21 24 24].

Cách 2: Đóng gói hàm broadcast_add vector hóa một dòng với toán tử np.newaxis ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

A = np.array([[10, 12, 11], [20, 21, 24], [30, 33, 31], [40, 44, 42]], dtype=np.int64)

b = np.array([1, 2, 3])

d = np.array([10, 20, 30, 40])

# Bắt đầu lời giải

import numpy as np


def broadcast_add(
    matrix: np.ndarray, column_add: np.ndarray, row_add: np.ndarray
) -> np.ndarray:
    """Cộng vector vào cột và hàng của ma trận bằng cơ chế broadcasting vector hóa.

    Tham số:
        matrix: Mảng hai chiều kiểu số hữu hạn không rỗng.
        column_add: Mảng 1 chiều có độ dài bằng số cột của matrix.
        row_add: Mảng 1 chiều có độ dài bằng số hàng của matrix.

    Trả về:
        Mảng hai chiều mới có cùng shape với matrix.
    """
    if matrix.ndim != 2:
        raise ValueError("Yêu cầu ma trận đầu vào phải có 2 chiều.")
    nrows, ncols = matrix.shape

    col_vec = np.asarray(column_add)
    row_vec = np.asarray(row_add)

    if col_vec.ndim != 1 or col_vec.size != ncols:
        raise ValueError(
            f"Vector cột cần có 1 chiều độ dài {ncols}, hiện tại {col_vec.shape}."
        )
    if row_vec.ndim != 1 or row_vec.size != nrows:
        raise ValueError(
            f"Vector hàng cần có 1 chiều độ dài {nrows}, hiện tại {row_vec.shape}."
        )

    # Áp dụng biểu thức vector hóa một dòng duy nhất
    # row_vec[:, np.newaxis] mở rộng shape từ (nrows,) thành (nrows, 1)
    return matrix + col_vec + row_vec[:, np.newaxis]


ket_qua = broadcast_add(A, b, d)
np.testing.assert_array_equal(ket_qua[0], [21, 24, 24])
print(ket_qua)

# Kết thúc lời giải

Giải thích cách 2 ​

broadcast_add nhận ma trận cùng vector cộng theo cột và vector cộng theo hàng. nrows, ncols = matrix.shape đọc kích thước thực tế. np.asarray cho phép chuẩn hóa hai vector đầu vào thành ndarray trước khi kiểm tra chúng có một trục và đủ số phần tử.

Biểu thức row_vec[:, np.newaxis] đổi shape (nrows,) thành (nrows, 1) để mỗi hàng nhận đúng một số cộng. Hàm trả matrix + col_vec + row_vec[:, np.newaxis], tạo kết quả mới và không gán vào ma trận gốc. Việc kiểm tra độ dài hai vector ngăn trường hợp phép toán vẫn chạy nhưng áp dụng sai ý nghĩa hàng/cột.

Kết quả cách 2 ​

HàngK, cột 0K, cột 1K, cột 2
0212424
1414347
2616564
3818685

So sánh hai cách ​

Cách 1 chia hai phép cộng để quan sát các shape trung gian. Cách 2 kiểm tra tham số rồi viết biểu thức tổng hợp dùng lại cho bảng khác. Cả hai dùng broadcasting và cho cùng K, không cần tạo bản lặp đầy đủ của các vector bằng tile.


Bài 6: Chuẩn hóa dữ liệu theo hàng và khắc phục lỗi sai trục axis ​

Mục tiêu: Phát hiện và khắc phục lỗi logic tiềm ẩn khi thực hiện phép toán thu gọn (reduction) theo trục axis kết hợp với broadcasting, hiểu rõ tầm quan trọng của việc duy trì số chiều không gian (keepdims).

Dữ liệu đầu vào: Bảng giá giả lập của 3 chỗ ở qua 3 kỳ thu thập (đơn vị USD/đêm):

python
import numpy as np

gia = np.array([[40, 50, 60], [60, 60, 60], [30, 60, 90]], dtype=np.float64)

Mảng gia tương ứng với ma trận G:

G=[40.050.060.060.060.060.030.060.090.0]

Tình huống lỗi thực tế: Đoạn mã dưới đây thực thi thành công mà không báo lỗi, nhưng kết quả tính toán lại sai lệch hoàn toàn so với yêu cầu toán học:

python
tb_sai = gia.mean(axis=1)
sai = gia - tb_sai

Thao tác yêu cầu:

  1. Phân tích kích thước (shape) của tb_sai và chỉ ra cơ chế phát sóng ngầm định đã khiến phép tính gia - tb_sai bị sai bản chất như thế nào.
  2. Sửa lại đoạn mã để tính toán chính xác ma trận độ lệch chuẩn hóa chenh_lech, sao cho giá của mỗi chỗ ở được trừ đi đúng giá trị trung bình của chính chỗ ở đó.
  3. Kiểm tra điều kiện: Giá trị trung bình của từng hàng sau khi chuẩn hóa phải xấp xỉ 0 (np.allclose(chenh_lech.mean(axis=1), 0)).
  4. Đóng gói hàm center_rows(prices: np.ndarray) -> tuple trả về bộ đôi (trung_binh_hang, chenh_lech) với trung_binh_hang có shape (N, 1) và chenh_lech có cùng kích thước với mảng đầu vào.

Trường hợp biên cần chú ý:

  • Hỗ trợ ma trận không vuông (số hàng khác số cột).
  • Với ma trận chỉ có 1 cột, độ lệch của mọi phần tử phải bằng 0.
Lời giải

Cách 1: Sửa lỗi trực tiếp bằng tham số keepdims=True ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

# Bắt đầu lời giải

import numpy as np

gia = np.array([[40, 50, 60], [60, 60, 60], [30, 60, 90]], dtype=np.float64)

# Sửa lỗi: Giữ nguyên số chiều không gian (keepdims=True) để có shape (3, 1)
trung_binh_hang = gia.mean(axis=1, keepdims=True)

# Thực hiện phép trừ broadcasting chuẩn xác theo hàng
chenh_lech = gia - trung_binh_hang

# Kết thúc lời giải

print("Row means:", trung_binh_hang)
print("Centered values:", chenh_lech)

Giải thích cách 1 ​

gia.mean(axis=1) thu gọn trục cột, tạo một trung bình cho mỗi hàng. Nếu không giữ số trục, kết quả có shape (3,). Khi trừ khỏi bảng vuông (3, 3), NumPy ghép trục cuối nên vô tình trừ theo cột, dù các số là trung bình hàng.

keepdims=True giữ trục đã thu gọn với độ dài 1, nên trung_binh_hang có shape (3, 1). Bây giờ mỗi hàng trừ đúng trung bình của chính nó. Hàng [40, 50, 60] trừ 50 thành [-10, 0, 10], hàng hằng 60 thành toàn 0, và hàng [30, 60, 90] thành [-30, 0, 30]. Đây là phép đưa tâm mỗi hàng về không, không phải chia cho độ lệch chuẩn.

Kết quả cách 1 ​

HàngTrung bìnhĐộ lệch cột 0Độ lệch cột 1Độ lệch cột 2
050.0−10.00.010.0
160.00.00.00.0
260.0−30.00.030.0

trung_binh_hang.shape bằng (3, 1), còn chenh_lech.shape bằng (3, 3). Trung bình của ba hàng độ lệch đều bằng 0.0.

Cách 2: Đóng gói hàm center_rows tổng quát hóa với kỹ thuật mở rộng trục lát cắt ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

gia = np.array([[40, 50, 60], [60, 60, 60], [30, 60, 90]], dtype=np.float64)

# Bắt đầu lời giải

import numpy as np


def center_rows(prices: np.ndarray) -> tuple:
    """Chuẩn hóa dữ liệu theo hàng bằng cách trừ đi trung bình của mỗi hàng.

    Tham số:
        prices: Mảng hai chiều kiểu số thực hoặc số nguyên, không rỗng.

    Trả về:
        Tuple gồm (trung_binh_hang, chenh_lech):
        - trung_binh_hang: Mảng 2D float có shape (số_hàng, 1).
        - chenh_lech: Mảng 2D float có cùng shape với prices.
    """
    arr = np.asarray(prices, dtype=np.float64)
    if arr.ndim != 2:
        raise ValueError("Yêu cầu mảng đầu vào phải có đúng 2 chiều.")
    if arr.size == 0:
        raise ValueError("Mảng đầu vào không được rỗng.")

    # Tính trung bình theo hàng và định hình lại thành vector cột (N, 1)
    row_means = arr.mean(axis=1)[:, np.newaxis]
    deviations = arr - row_means

    return row_means, deviations


trung_binh, do_lech = center_rows(gia)
print("Trung bình từng hàng:", trung_binh)
print("Độ lệch:", do_lech)

# Kết thúc lời giải

Giải thích cách 2 ​

np.asarray(prices, dtype=np.float64) chuẩn hóa đầu vào sang kiểu số thực để phép lấy trung bình phù hợp cả dữ liệu nguyên. Hàm kiểm tra hai trục và không rỗng trước khi tính. arr.mean(axis=1)[:, np.newaxis] thực hiện hai bước tương đương keepdims: Tính trung bình hàng rồi thêm một trục có độ dài 1.

Hàm trả tuple (row_means, deviations), vì vậy nơi gọi dùng hai tên để nhận hai mảng. Phép trừ tạo mảng kết quả mới, không sửa prices. Khi kiểm tra số thực, np.allclose(do_lech.mean(axis=1), 0) phù hợp hơn yêu cầu mọi giá trị phải bằng không tuyệt đối, vì phép tính dấu phẩy động có thể để lại sai số nhỏ.

Kết quả cách 2 ​

HàngTrung bìnhĐộ lệch cột 0Độ lệch cột 1Độ lệch cột 2
050.0−10.00.010.0
160.00.00.00.0
260.0−30.00.030.0

np.allclose(do_lech.mean(axis=1), 0) cho True.

So sánh hai cách ​

Cách 1 giữ trục ngay khi thu gọn bằng keepdims. Cách 2 thêm lại trục sau khi thu gọn và gom kiểm tra đầu vào vào hàm. Cả hai làm cùng một phép biến đổi. Bảng vuông che giấu lỗi shape, nên hãy kiểm tra thêm bảng không vuông khi dùng lại hàm.


Bài 7: Phân tích hiệu năng: Vòng lặp Python thuần và vector hóa NumPy ​

Mục tiêu: Đo lường và giải thích cặn kẽ bản chất chênh lệch hiệu năng giữa vòng lặp thông dịch của Python thuần và phép toán vector hóa trên mảng NumPy, phân tích dưới góc độ kiến trúc vùng nhớ liền kề (contiguous buffer) và tối ưu hóa phần cứng (SIMD).

Dữ liệu đầu vào: Xét bài toán nhân đôi mọi phần tử của một dãy số nguyên:

  1. Phương pháp 1: Vòng lặp comprehension trên danh sách Python (nhan_list).
  2. Phương pháp 2: Vòng lặp comprehension trên đối tượng ndarray (nhan_for_array).
  3. Phương pháp 3: Phép toán vector hóa mảng x * 2 trên ndarray (nhan_array).

Thao tác yêu cầu:

  1. Triển khai cả ba phương pháp và kiểm tra tính tương đương kết quả số học trên mảng nhỏ (N=10) và mảng lớn (N=100000).
  2. Đo thời gian thực thi bằng hàm timeit.repeat (lấy thời gian nhỏ nhất trong 3 lần đo lặp lại và nêu giới hạn của phép đo), lập bảng đối chiếu kết quả đo đạc.
  3. Trả lời ba câu hỏi phân tích bản chất:
    • Câu hỏi 1: Phương pháp nào vẫn chạy vòng lặp bên trong tầng thông dịch của Python?
    • Câu hỏi 2: Việc chuyển đổi danh sách Python thành đối tượng ndarray nhưng vẫn dùng vòng lặp for có giúp chương trình chạy nhanh hơn không? Giải thích nguyên nhân dưới góc độ đóng gói đối tượng (boxing/unboxing).
    • Câu hỏi 3: Tỷ lệ chênh lệch thời gian giữa các phương pháp thay đổi như thế nào khi tăng kích thước từ N=10 lên N=100000? Giải thích dựa trên chi phí thông dịch (interpreter overhead) và vector hóa phần cứng (SIMD).

Trường hợp biên cần chú ý:

  • Dữ liệu thử nghiệm phải được chuẩn bị hoàn tất trước khi kích hoạt bộ đếm thời gian.
  • Không so sánh tỷ lệ tốc độ bằng các con số giả định, mà phải dựa trên số đo thực tế của môi trường chạy.
Lời giải

Cách 1: Đo lường thực nghiệm trực tiếp bằng timeit.repeat ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

# Bắt đầu lời giải

import numpy as np
from timeit import repeat


def nhan_list(xs):
    return [v * 2 for v in xs]


def nhan_for_array(x):
    return [v * 2 for v in x]


def nhan_array(x):
    return x * 2


# Chạy thử nghiệm và đo lường trên hai quy mô kích thước
for n in (10, 100_000):
    x = np.arange(n, dtype=np.int64)
    xs = x.tolist()

    # Kiểm tra tính tương đương số học
    assert nhan_list(xs) == nhan_array(x).tolist()
    assert nhan_for_array(x) == nhan_array(x).tolist()

    so_lan = 100 if n == 10 else 5
    print(f"Quy mô dữ liệu n = {n:,}:")
    for ten, ham, dau_vao in [
        ("Vòng for trên list", nhan_list, xs),
        ("Vòng for trên ndarray", nhan_for_array, x),
        ("Phép toán vector hóa ndarray", nhan_array, x),
    ]:
        t = min(repeat(lambda: ham(dau_vao), number=so_lan, repeat=3)) / so_lan
        print(f"  {ten}: {t * 1e6:.2f} us/lan")

# Kết thúc lời giải

Giải thích cách 1 ​

Ba hàm đều nhân mỗi số với 2, nhưng hai comprehension vẫn lặp ở mức Python. Đổi thành ndarray rồi tiếp tục dùng for chưa loại bỏ việc xử lý từng phần tử trong Python. Với dtype số của mẫu, biểu thức x * 2 giao phép toán mảng cho vòng lặp đã biên dịch của NumPy.

repeat(..., number=so_lan, repeat=3) đo ba đợt, mỗi đợt thực hiện so_lan lời gọi. Mỗi số trả về là thời gian của cả đợt, nên cần chia cho so_lan để ra thời gian một lượt. min chọn đợt nhanh nhất để giảm ảnh hưởng của một số đợt bị trì hoãn, không loại sạch mọi nhiễu. Mảng và được tạo trước phép đo, nên số đo chưa gồm thời gian chuyển đổi dữ liệu.

Kết quả cách 1 ​

Lần chạy kiểm chứng cho các số dưới đây, đơn vị µs/lượt. Đây là đầu ra minh họa của lần đo, không phải hằng số kỳ vọng:

nComprehension trên Comprehension trên ndarrayx * 2
100.231.050.62
1000002758.286202.0837.40

Cách 2: Xây dựng hàm đo chuẩn hóa benchmark_vectorization tự động tính hệ số gia tốc ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

# Bắt đầu lời giải

import numpy as np
from timeit import repeat


def benchmark_vectorization(sizes=(10, 100_000)) -> dict:
    """Đo lường thời gian thực thi và tính toán hệ số gia tốc của vector hóa.

    Tham số:
        sizes: Tuple các quy mô kích thước mảng cần khảo sát.

    Trả về:
        Từ điển chứa thời gian thực thi (µs) và tỷ số gia tốc (speedup) cho từng quy mô.
    """
    results = {}

    for n in sizes:
        x = np.arange(n, dtype=np.int64)
        xs = x.tolist()

        runs = 100 if n <= 1000 else 5
        t_list = min(repeat(lambda: [v * 2 for v in xs], number=runs, repeat=3)) / runs
        t_for_arr = (
            min(repeat(lambda: [v * 2 for v in x], number=runs, repeat=3)) / runs
        )
        t_vec = min(repeat(lambda: x * 2, number=runs, repeat=3)) / runs

        results[n] = {
            "list_comprehension_us": t_list * 1e6,
            "for_on_ndarray_us": t_for_arr * 1e6,
            "vectorized_us": t_vec * 1e6,
            "speedup_vs_list": t_list / t_vec,
            "speedup_vs_for_ndarray": t_for_arr / t_vec,
        }

    return results


print(benchmark_vectorization(sizes=(10, 100000)))

# Kết thúc lời giải

Giải thích cách 2 ​

benchmark_vectorization nhận các kích thước ở sizes và trả một từ điển có khóa là kích thước. Với mỗi kích thước, hàm chuẩn bị ndarray và tương ứng, chọn số lượt đo rồi ghi thời gian của ba thao tác dưới dạng µs/lượt. lambda tạo một lời gọi không đối số để bộ đo thực thi lặp lại.

speedup_vs_list lấy thời gian của chia thời gian vector hóa. Tỷ số lớn hơn 1 nghĩa là vector hóa nhanh hơn trong đúng phép đo đó, nhỏ hơn 1 nghĩa là chậm hơn. NumPy có chi phí khởi động phép toán và cấp mảng kết quả, nên với mảng rất nhỏ chi phí này có thể chiếm phần lớn thời gian. Việc có dùng SIMD hay không phụ thuộc phép toán, bản dựng và CPU, không được suy ra chỉ từ tên ndarray.

Kết quả cách 2 ​

Lần chạy hàm kiểm chứng tạo các giá trị sau, đã làm tròn để đọc:

n (µs)For ndarray (µs)Vector (µs)Tỷ số /vector
100.1990.9950.5970.33
1000002825.646459.5632.5886.73

Ở n = 100000, tỷ số vòng for trên ndarray so với vector khoảng 198.27 trong lần đo này.

So sánh hai cách ​

Hai cách đo cùng ba thao tác. Cách 1 in bảng ngay, còn Cách 2 giữ số đo và tỷ số trong một cấu trúc để xử lý tiếp. Không so các con số giữa hai lần chạy như thể môi trường không đổi. Khi đánh giá cả quy trình, cần đo thêm chi phí đọc dữ liệu, chuyển kiểu và chuyển thành mảng.


Bài 8: Thống kê mô tả dữ liệu giá và lấy mẫu ngẫu nhiên không hoàn lại ​

Mục tiêu: Tính trung bình, trung vị và độ lệch chuẩn của một dãy giá. Quan sát tác động của giá cao lên các chỉ số, rồi lấy mẫu ngẫu nhiên không hoàn lại bằng bộ sinh của NumPy.

Dữ liệu đầu vào: Dãy giá của 5 chỗ ở giả lập là [40, 50, 60, 70, 380], tính bằng USD mỗi đêm.

Thao tác yêu cầu:

  1. Tính toán giá trị trung bình (mean), trung vị (median), và độ lệch chuẩn tổng thể (std với bậc tự do ddof=0) của mảng bằng NumPy.
  2. Tính tỷ lệ phần trăm (share) các chỗ ở có mức giá không vượt quá 70 USD/đêm.
  3. Rút ngẫu nhiên một mẫu gồm 3 chỗ ở không hoàn lại (replace=False) sử dụng bộ tạo số ngẫu nhiên np.random.default_rng(seed=42), trích xuất các giá trị trong mẫu và tính trung bình mẫu.
  4. Đóng gói quy trình trên thành hàm price_statistics(prices: np.ndarray, threshold: float = 70.0, sample_size: int = 3, seed: int = 42) -> dict tuân thủ đúng quy chuẩn dữ liệu.
  5. Trả lời cặn kẽ bốn câu hỏi định tính thực tiễn trong phân tích dữ liệu:
    • Trong báo cáo thực tế, nên dùng giá trị trung bình hay trung vị để mô tả mức giá điển hình của tập dữ liệu này? Vì sao?
    • Có nên loại bỏ ngay giá trị 380 USD/đêm chỉ vì nó cao vượt trội so với các quan sát còn lại hay không?
    • Giá trị trung bình mẫu rút ra có nhất thiết phải bằng trung bình của toàn bộ 5 chỗ ở không? Việc cố định hạt giống seed có bảo đảm mẫu thu được mang tính đại diện hoàn hảo hay không?
    • Khi đã có toàn bộ dữ liệu hợp lệ trong bộ nhớ, việc lấy mẫu ngẫu nhiên có cần thiết để tính giá trung bình không? Khi nào việc trích xuất một mẫu nhỏ vẫn mang lại giá trị kỹ thuật thực tế?

Trường hợp biên cần chú ý:

  • Không sửa đổi mảng đầu vào và không can thiệp vào bộ sinh số ngẫu nhiên toàn cục.
  • Giữ cùng phiên bản NumPy, cấu hình bộ sinh, seed và chuỗi lời gọi để đối chiếu kết quả giữa các lần chạy.
Lời giải

Cách 1: Tính toán tuần tự bằng các hàm thống kê cơ bản của NumPy ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

# Bắt đầu lời giải

import numpy as np

gia_dem = np.array([40, 50, 60, 70, 380], dtype=np.float64)

# 1. Tính toán các đại lượng thống kê mô tả
trung_binh = float(np.mean(gia_dem))
trung_vi = float(np.median(gia_dem))
do_lech_chuan = float(np.std(gia_dem, ddof=0))

# 2. Tính tỷ lệ giá không vượt quá 70 USD
ty_le = float(np.mean(gia_dem <= 70.0))

# 3. Lấy mẫu ngẫu nhiên không hoàn lại 3 phần tử với seed 42
rng = np.random.default_rng(42)
chi_muc = rng.choice(len(gia_dem), size=3, replace=False)
mau = gia_dem[chi_muc]
tb_mau = float(np.mean(mau))

# Kết thúc lời giải

print(
    {
        "mean": trung_binh,
        "median": trung_vi,
        "std": do_lech_chuan,
        "share": ty_le,
        "indices": chi_muc,
        "sample": mau,
        "sample_mean": tb_mau,
    }
)

Giải thích cách 1 ​

np.mean, np.median và np.std(..., ddof=0) lần lượt tính trung bình, trung vị và độ lệch chuẩn tổng thể. Tổng năm giá bằng 600, nên trung bình là 120. Trung vị là phần tử thứ ba sau sắp xếp, bằng 60. Tổng bình phương độ lệch so với 120 bằng 85000, chia 5 được phương sai 17000, lấy căn cho độ lệch chuẩn khoảng 130.384 USD.

gia_dem <= 70.0 tạo bốn giá trị True và một False, nên trung bình Boolean bằng 0.8. default_rng(42) tạo một bộ sinh cục bộ. choice(len(gia_dem), size=3, replace=False) chọn ba chỉ mục khác nhau, rồi lập chỉ mục lấy ba giá tương ứng. Với chuỗi lời gọi và phiên bản NumPy đang kiểm tra, các chỉ mục là [4, 0, 3].

Kết quả cách 1 ​

Chỉ sốKết quả
mean120.0
median60.0
std, ddof=0130.38404810405297
share0.8 (80%)
indices[4, 0, 3]
sample[380.0, 40.0, 70.0]
sample_mean163.33333333333334

Cách 2: Đóng gói hàm price_statistics kiểm soát ràng buộc dữ liệu và hạt giống ngẫu nhiên ​

python
# Dữ liệu mẫu và các hàm hỗ trợ

import numpy as np

gia_dem = np.array([40, 50, 60, 70, 380], dtype=np.float64)

# Bắt đầu lời giải

import numpy as np


def price_statistics(
    prices: np.ndarray, threshold: float = 70.0, sample_size: int = 3, seed: int = 42
) -> dict:
    """Tính toán thống kê mô tả và trích xuất mẫu ngẫu nhiên không hoàn lại.

    Tham số:
        prices: Mảng 1 chiều chứa các mức giá không âm, không rỗng.
        threshold: Ngưỡng giá cần tính tỷ lệ phần trăm.
        sample_size: Kích thước mẫu cần rút (1 <= sample_size <= len(prices)).
        seed: Hạt giống sinh số ngẫu nhiên không âm.

    Trả về:
        Từ điển chứa mean, median, std, share, indices, sample và sample_mean.
    """
    arr = np.asarray(prices, dtype=np.float64)
    if arr.ndim != 1 or arr.size == 0:
        raise ValueError("Mảng giá phải là mảng 1 chiều không rỗng.")
    if not (1 <= sample_size <= arr.size):
        raise ValueError(
            f"sample_size {sample_size} phải nằm trong khoảng [1, {arr.size}]."
        )

    # Tính các tham số thống kê mô tả tổng thể
    mean_val = float(np.mean(arr))
    median_val = float(np.median(arr))
    std_val = float(np.std(arr, ddof=0))
    share_val = float(np.mean(arr <= threshold))

    # Rút mẫu ngẫu nhiên độc lập
    rng = np.random.default_rng(seed)
    indices = rng.choice(len(arr), size=sample_size, replace=False)
    sample_arr = arr[indices]
    sample_mean_val = float(np.mean(sample_arr))

    return {
        "mean": mean_val,
        "median": median_val,
        "std": std_val,
        "share": share_val,
        "indices": indices,
        "sample": sample_arr,
        "sample_mean": sample_mean_val,
    }


print(price_statistics(gia_dem))

# Kết thúc lời giải

Giải thích cách 2 ​

Hàm nhận giá, ngưỡng, kích thước mẫu và seed, rồi trả từ điển chứa cả thống kê lẫn các mảng mẫu. np.asarray(..., dtype=np.float64) chuẩn hóa kiểu số. Hai kiểm tra tiếp theo yêu cầu mảng một trục không rỗng và số phần tử mẫu nằm từ 1 đến kích thước mảng.

Các kết quả thống kê được chuyển thành float Python, còn indices và sample vẫn là ndarray. replace=False cấm chọn lại cùng một chỉ mục, không cấm hai quan sát khác nhau có cùng giá. Seed chỉ giúp lặp lại lựa chọn khi giữ cùng cấu hình bộ sinh và chuỗi lời gọi. Nó không bảo đảm một mẫu ba dòng đại diện hoàn hảo, cũng không tự kiểm chứng giá cao là sai dữ liệu.

Kết quả cách 2 ​

Chỉ sốKết quả
mean120.0
median60.0
std, ddof=0130.38404810405297
share0.8 (80%)
indices[4, 0, 3]
sample[380.0, 40.0, 70.0]
sample_mean163.33333333333334

So sánh hai cách ​

Cách 1 cho thấy từng biến trung gian, Cách 2 đóng gói cùng quy trình với tham số và kiểm tra kích thước. Trung vị 60 mô tả vị trí giữa của năm giá, còn trung bình 120 tính cả ảnh hưởng của giá 380. Chưa có bằng chứng để xóa 380 hoặc gán nó cho một loại chỗ ở cụ thể. Nếu đã có toàn bộ năm giá, có thể tính trung bình trực tiếp mà không cần lấy mẫu.


Tổng kết bài học ​

Chủ đề kỹ thuậtKhái niệm trọng tâmBản chất cần ghi nhớ
Cấu trúc mảngshape, dtype, strides, nbytesMảng C-contiguous như mảng A ban đầu lưu trữ dữ liệu liền kề, còn các view bước nhảy có thể không liên tục.
Lát cắt và viewA[1::2, 0::2], np.shares_memoryView dùng chung dữ liệu với mảng gốc. Nó vẫn cần bộ nhớ cho thông tin quản lý, nhưng không sao chép toàn bộ phần tử. Sửa view có thể làm đổi mảng gốc.
Phân biệt view và copyLập chỉ mục lát cắt vs Lập chỉ mục mảngLát cắt cơ bản tạo view. Lấy phần tử bằng danh sách hoặc mảng chỉ mục nâng cao tạo bản sao dữ liệu.
Kỹ thuật chọn lọcMặt nạ boolean, mảng chỉ mục, np.ix_Mặt nạ cùng shape với A lấy các phần tử thành mảng 1D. Hai mảng chỉ mục có thể chọn các cặp tọa độ hoặc tạo lưới giao điểm.
Phát sóng (broadcasting)Trailing dimension alignment, np.newaxisSo sánh các trục từ phải qua trái. Hai độ dài phải bằng nhau hoặc một trong hai bằng 1 để phép toán mảng có thể broadcasting.
Hiệu năng thực thiVòng lặp ở mã máy, chi phí xử lý từng phần tửNhiều phép toán trên dtype số dùng vòng lặp đã biên dịch. Việc dùng SIMD phụ thuộc phép toán, bản dựng và CPU. Cần đo cả chi phí chuẩn bị dữ liệu khi so sánh toàn quy trình.
Thống kê và lấy mẫuMedian vs Mean, Generator, OutliersTrung vị ít nhạy với độ lớn của một giá cực đoan. Seed giúp tái lập lựa chọn khi giữ cùng cấu hình và chuỗi lời gọi, nhưng không bảo đảm tính đại diện của mẫu.

Tài liệu tham khảo ​

  1. McKinney, W. (2022). Python for Data Analysis: Data Wrangling with pandas, NumPy, and Jupyter (3rd ed.). O'Reilly Media.
  2. Oliphant, T. E. (2006). A Guide to NumPy. Trelgol Publishing.
  3. NumPy Developers. (2026). NumPy Reference and User Guide (Version 2.x). https://numpy.org/doc/stable/
  4. Harris, C. R., Millman, K. J., van der Walt, S. J., et al. (2020). Array programming with NumPy. Nature, 585(7825), 357–362.
  5. Inside Airbnb. (2026). Santiago, Chile Data Listings & Visualisations Snapshot. https://data.insideairbnb.com/chile/rm/santiago/

Tài liệu tham khảo ​

Ảnh & sơ đồ
100%
Ảnh tài liệu được phóng to