Masks và Boolean Arrays trong NumPy - ài liệu học Numpy từ cơ bản đến nâng cao
Trong bài trước ta đã học được các thao tác tính toán trên mảng từ cơ bản đến nâng cao với NumPy. Trong việc tính toán và xử lý dữ liệu, thì lọc dữ liệu bị thiếu hoặc tính toán dựa trên một số điều kiện nhất định là rất quan trọng, và để có được điều đó thì ta thường phải làm việc với các phép toán ...
Trong bài trước ta đã học được các thao tác tính toán trên mảng từ cơ bản đến nâng cao với NumPy. Trong việc tính toán và xử lý dữ liệu, thì lọc dữ liệu bị thiếu hoặc tính toán dựa trên một số điều kiện nhất định là rất quan trọng, và để có được điều đó thì ta thường phải làm việc với các phép toán so sánh.
So sánh thì đương nhiên sẽ trả về kết quả là True hoặc False (Boolean). Trong bài này, ta sẽ học cách xử lý trên các mảng Boolean (Boolean Array) và áp dụng vào Masks, một tính năng rất hữu ích trong NumPy.
1. Giới thiệu về Masks
Ví dụ mở đầu
Thay vì lý thuyết phức tạp thì ta sẽ đến một ví dụ nhỏ để hiểu về Masks. Giả sử ta có một danh sách người truy cập Zaidap.com trong 7 ngày gần nhất là:
visitors = np.array([5956, 1975, 4567, 7597, 2431, 2675, 3270])
Tính tổng số lượt truy cập của các ngày có số lượng truy cập > 5000.
Nếu dùng Python thuần tuý, nó sẽ có dạng như sau:
total = 0 for num in visitors: if (num > 5000): total += num print(total)
13553
Trong bài toán này, ta có thể dùng Masks để đơn giản hoá cách giải quyết. Về cơ bản, nó dùng một mảng Boolean (True/False) khi được áp lên mảng gốc sẽ trả về cho ta các giá trị cần quan tâm. Phần tử có giá trị là True sẽ thoả mãn điều kiện và ngược lại, False thì không. Trước tiên ta tạo một mảng mask:
mask = [True, False, False, True, False, False, False]
Mảng mask sẽ có cùng số phần tử với mảng visitors, và mỗi vị trí tương ứng của mảng mask sẽ cho ta biết phần tử ở mảng visitors có thoả mãn điều kiện (>5000) hay không.
Tiếp theo, ta sẽ áp mảng mask lên mảng visitors với cú pháp tương tự như đang truy cập vào phần tử của mảng:
visitors[mask]
array([5956, 7597])
Như ta thấy, nó sẽ trả về một mảng mới thoả mãn điều kiện ban đầu, giờ ta chỉ cần áp dụng thêm phương thức sum sẵn có của NumPy:
visitors[mask].sum()
13553
Qua ví dụ trên, ta đã hiểu được cơ bản cơ chế của Masks trong NumPy, tuy nhiên giả sử như mảng visitors không phải chứa 7 phần tử mà là 7000 phần tử thì sao? Ta không thể ngồi đếm từng cái một được, vì vậy ta sẽ cùng tìm hiểu về các toán tử so sánh trong NumPy.
Toán tử so sánh (Comparison Operators)
Trong bài trước, mình đã giới thiệu việc NumPy có thể sử dụng các toán tử đại số như +, -, *, /,... với mảng như là 2 số bình thường, thì với các phép so sánh như >, <,... cũng vậy, và mảng trả về sẽ là một mảng boolean:
x = np.arange(6) print(x) print("x < 3: ", x < 3) print("x <= 3: ", x <= 3) print("x >= 3: ", x >= 3) print("x == 3: ", x == 3) print("x != 3: ", x != 3)
[0 1 2 3 4 5] x < 3: [ True True True False False False] x <= 3: [ True True True True False False] x >= 3: [False False False True True True] x == 3: [False False False True False False] x != 3: [ True True True False True True]
Và nó cũng có thể áp dụng cho một biểu thức, chẳng hạn:
print("x * 2 = ", x * 2) print("x ** 2 = ", x ** 2) (x * 2) == (x ** 2)
x * 2 = [ 0 2 4 6 8 10] x ** 2 = [ 0 1 4 9 16 25] array([ True, False, True, False, False, False])
Tương tự như các phép toán đại số, các phép so sánh cũng có ufunc tương ứng trong bảng sau:
2. Các thao tác với Boolean Arrays
Đếm số phần tử
Giả sử ta có một mảng chứa tuổi của một số người như sau:
tuoi = [32, 35, 8, 25, 19, 23, 31, 31, 36, 27, 33, 18, 39, 9, 11]
Làm sao để đếm được những người có tuổi lớn hơn 30 và bé hơn 10?
Đầu tiên, ta sẽ áp dụng toán tử so sánh đã nói trên để lấy một mảng boolean thoả mãn điều kiện bài toán
tuoi = np.array([32, 35, 8, 25, 19, 23, 31, 31, 36, 27, 33, 18, 39, 9, 11]) boolArr = (tuoi > 30) | (tuoi < 9) boolArr
array([ True, True, True, False, False, False, True, True, True, False, True, False, True, False, False]) # True tương ứng với người có tuổi lớn hơn 30 hoặc bé hơn 9
NumPy hỗ trợ một phương thức đếm các giá trị khác không là np.count_nonzero, và như ta đã biết ở trong hầu hết ngôn ngữ lập trình thì giá trị của False sẽ bằng 0 còn True là một, Python cũng không ngoại lệ:
print(False == 0) # True
Vì vậy ta có thể áp dụng để đếm số biến có giá trị bằng True (tức thoả mãn điều kiện trên):
np.count_nonzero(boolArr)
8
Để ngắn gọn hơn, ta hoàn toàn có thể viết như sau:
np.count_nonzero((tuoi > 30) | (tuoi < 9))
8
Nhớ lại ví dụ đầu bài về Masks, ta hoàn toàn có thể áp dụng phương thức sum trong trường hợp này (do các biến False = 0 và True = 1, nên cộng các giá trị của mảng lại cũng giống như đếm các giá trị khác 0):
np.sum((tuoi > 30) | (tuoi < 9))
8
Một điểm mạnh của phương thức này là bạn có thể thao tác với các trục khác nhau, giả sử:
# Cho mảng x, đếm có bao nhiêu phần tử > 5 ở mỗi cột và hàng x = np.array([[3, 1, 0], [2, 6, 8], [6, 9, 7]]) # Sử dụng phương thức sum kèm theo tham số axis (trục) tương ứng print("Số phần tử > 5 theo mỗi hàng:", np.sum(x > 5, axis=1)) print("Số phần tử > 5 theo mỗi cột:", np.sum(x > 5, axis=0))
Số phần tử > 5 theo mỗi hàng: [0 2 3] Số phần tử > 5 theo mỗi cột: [1 2 2]
Nếu ta muốn kiểm tra nhanh xem tất cả các giá trị trong mảng có thoả mãn một điều kiện hay không thì có thể sử dụng np.all và np.any:
print("Trong mảng x có phần tử > 7: ", np.any(x > 8)) print("Trong mảng x có phần tử < 0: ", np.any(x < 0)) print("Trong mảng x tất cả các phần tử đều < 10: ", np.all(x < 10)) print("Trong mảng x tất cả các phần tử đều = 3", np.all(x == 0))
Trong mảng x có phần tử > 7: True Trong mảng x có phần tử < 0: False Trong mảng x tất cả các phần tử đều < 10: True Trong mảng x tất cả các phần tử đều = 3 False
Ta cũng có thể dùng cho từng trục cụ thể:
print("Theo từng cột trong mảng x, cột nào có tất cả các phần tử lớn hơn 1:", np.all(x > 1, axis=0))
Theo từng cột trong mảng x, cột nào có tất cả các phần tử lớn hơn 1: [ True False False]
Toán tử Boolean
Hầu như trong series làm với NumPy thì ai cũng đã học về lập trình Python cơ bản cũng như các toán tử Boolean trong Python, vì vậy mình sẽ không cần giải thích từng cái nữa. Dưới đây bảng toán tử tương ứng với từng ufunc cụ thể:
3. Module MaskedArray trong NumPy
NumPy hỗ trợ một module là MaskedArray được import bằng cú pháp:
import numpy.ma as ma
Hoặc ta có thể sử dụng trực tiếp như thế này cũng được:
import numpy as np np.ma
Module này cho phép ta tạo trực tiếp một masked array với các phần tử không hợp lệ sẽ bị gắn nhãn là không hợp lệ. Một điều lưu ý là ngược lại so với thông thường, giá trị hợp lệ sẽ là = 0 và tương đương với False, còn giá trị không hợp lệ sẽ = 1 hoặc tương đương với True.
Ta có ví dụ sau:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8]) masked_arr = np.ma.masked_array(arr, mask=arr > 5) masked_arr
masked_array(data=[1, 2, 3, 4, 5, --, --, --], mask=[False, False, False, False, False, True, True, True], fill_value=999999)
Như ta thấy, mảng trả về là một mảng cùng shape với mảng ban đầu, trong đó các giá trị không phù hợp (tương ứng với mask = True) sẽ bị thay đổi giá trị thành "--". Trong ví dụ ở đầu bài, khi ta dùng cú pháp:
visitors[mask]
Thì mảng trả về sẽ không cùng kích thước với mảng ban đầu mà chỉ chứa những phần tử hợp lệ.
Nếu không quen với kiểu dùng ngược True/False này, thì ta có thể sử dụng toán tử ~ (XOR) để đảo ngược giá trị của True/False, chẳng hạn với ví dụ trên:
masked_arr = np.ma.masked_array(arr, mask=~(arr > 5)) masked_arr
masked_array(data=[1, 2, 3, 4, 5, --, --, --], mask=[False, False, False, False, False, True, True, True], fill_value=999999)
Vì mảng trả về cũng là một mảng NumPy, nên ta có thể áp dụng các phương thức quen thuộc của NumPy, ví dụ như:
# Tổng các phần tử trong mảng masked_arr.sum()
21
Như các bạn cũng có thể để ý, masked array trả về từ module MaskedArrays có chứa một tham số là fill_value. Đây là một điểm khá hay trong module này, đúng với cái tên của nó, nếu như bạn dùng phương thức filled lên mảng, nó sẽ điền vào các phần tử bị khuyết giá trị của fill_value, như:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8]) # Nếu không đặt fill_value từ đầu, mặc định là 999999 masked_arr = np.ma.masked_array(arr, mask=arr > 5, fill_value=1000) print("Masked Array chưa được điền: ", masked_arr) print("Masked Array đã điền: ", masked_arr.filled())
Masked Array chưa được điền: [1 2 3 4 5 -- -- --] Masked Array đã điền: [ 1 2 3 4 5 1000 1000 1000]
Hoặc ta có thể điền một giá trị tuỳ ý mà ta muốn:
# Điền vào phần tử bị khuyết giá trị trung bình của mảng ban đầu masked_arr.filled(arr.mean())
array([1, 2, 3, 4, 5, 4, 4, 4])
Tuỳ thuộc vào điều kiện của mask, NumPy cho ta khá nhiều phương thức hỗ trợ cho từng điều kiện cụ thể:
Nhỏ hơn / Nhỏ hơn bằng một số
Ta sử dụng phương thức masked_less cho dạng điều kiện này:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8]) masked_array = np.ma.masked_less(arr, 4) masked_array
masked_array(data=[--, --, --, 4, 5, 6, 7, 8], mask=[ True, True, True, False, False, False, False, False], fill_value=999999)
Tương tự với masked_less, nếu là nhỏ hơn bằng thì ta dùng masked_less_equal:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8]) masked_array = np.ma.masked_less_equal(arr, 4) masked_array
masked_array(data=[--, --, --, --, 5, 6, 7, 8], mask=[ True, True, True, True, False, False, False, False], fill_value=999999)
Lớn hơn (hoặc lớn hơn bằng) một số
Ta sử dụng phương thức masked_greater cho dạng điều kiện này:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8]) masked_array = np.ma.masked_greater(arr, 4) masked_array
masked_array(data=[1, 2, 3, 4, --, --, --, --], mask=[False, False, False, False, True, True, True, True], fill_value=999999)
Tương tự với masked_greater_equal.
Trong một phạm vi nhất định
Phương thức masked_inside sẽ lọc các dữ liệu trong một phạm vi nhất định:
arr = np.array([1, 2, 6, 4, 5, 9, 7, 8]) # Lọc các phần tử từ có giá trị trong khoảng 2 - 5 masked_array = np.ma.masked_inside(arr, 2, 5) masked_array
masked_array(data=[1, --, 6, --, --, 9, 7, 8], mask=[False, True, False, True, True, False, False, False], fill_value=999999)
Ngoài một phạm vi nhất định
Tương tự trên nhưng ngược lại, ta có phương thức masked_outside:
arr = np.array([1, 2, 6, 4, 5, 9, 7, 8]) masked_array = np.ma.masked_outside(arr, 2, 5) masked_array
masked_array(data=[--, 2, --, 4, 5, --, --, --], mask=[ True, False, True, False, False, True, True, True], fill_value=999999)
Trên là 4 phương thức phổ biến cho các điều kiện nhất định của masks, nếu bạn muốn tìm hiểu một số phương thức khác ít sử dụng hơn thì có thể tham khảo tại đây: The numpy.ma module — NumPy v1.19 Manual
4. Tổng kết
Qua bài trên bạn đã được tìm hiểu các thao tác với mảng Boolean, cũng như cách áp dụng nó với Masks. Masks là một tính năng cực kỳ hữu ích trong NumPy, giúp ta giảm thiểu code đi khá nhiều và tính toán trở nên thuận lợi hơn. Trong bài sau, ta sẽ tìm hiểu về Fancy Indexing trong NumPy - một tính năng sẽ sử dụng Masks khá nhiều. Hẹn gặp bạn ở bài tiếp.