🍀Tóm tắt

Trong buổi hôm trước chúng ta thực hành cách làm việc với Tệp tin và Hệ điều hành: Về cách làm sao ta có thể đọc và thao tác với dữ liệu trên file đó và để nội dung thêm chỉnh chu mình sẽ thử đọc các file ở các nền tảng lưu trữ khác như Sharepoint, Google Drive, Onedrive hay các dịch vụ Cloud Azure và AWS.

Bạn có thể xem lại buổi Live bữa trước ở đây

🔴Buổi Live ngày 9:

Livestream ngày 9

Làn này chúng ta sẽ thực sự bước chân vào con đường phân tích dữ liệu bằng việc tiếp cận một thư viện vô cùng nổi tiếng mà bất cứ ai làm việc trong các lĩnh vực tính toán, thống kê và phân tích đều quen thuộc: NumPy

🍀 Bàn về NumPy

🧠 Những gì bạn cần biết

NumPy, viết tắt từ Numerical Python, là packages quan trọng nhất cho lĩnh vực “Điện toán đại số” (numerical computing).

Numerical computing là một lĩnh vực trong khoa học máy tính và toán học tập trung vào việc sử dụng máy tính để thực hiện tính toán và xử lý dữ liệu số học. Lĩnh vực này tập trung vào việc phát triển các thuật toán, phương pháp và công cụ tính toán dành cho xử lý và phân tích dữ liệu số toán học, thống kê, các tính toán khoa học, …

Ứng dụng của nó đa dạng và phổ biến trong nhiều lĩnh vực bao gồm DS, Deep Learning, Machine Learning, Tài chính, …

Note 📙

NumPy được xem như một trong giao thức tiêu chuẩn để trao đổi dữ liệu giữa nhiều thư viện điện toán về xử lý khoa học.

Các cú pháp xử lý và biến đổi với NumPy vẫn có thể được áp dụng với pandas.

📚 Học gì về NumPy

Vì các chủ đề trong NumPy khá rộng, mình có liệt kê một số nội dung chính bên dưới (một số topics mình sẽ skip qua vì không liên quan lắm tới Phân tích dữ liệu):

  • ndarray: Là một cấu trúc mảng đa chiều, hỗ trợ các “phép toán đại số thực hiện trên các mảng” (array-oriented arithmetic operations) và khả năng “broadcasting” linh hoạt – Ý chỉ có thể tính toán qua lại giữa 2 mảng có kích thước (dimensions size) khác nhau
# Ví dụ về `array-oriented arithmetic operations`
import numpy as np

array1 = np.array([1, 2, 3, 4, 5])
array2 = np.array([6, 7, 8, 9, 10])

# Phép cộng mảng
result = array1 + array2
# result = [7, 9, 11, 13, 15]

# Ví dụ về `broadcasting capabilities`
# Chúng ta sẽ thêm vector v vào mỗi hàng của ma trận x,
# Lưu trữ kết quả trong ma trận y
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
y = np.empty_like(x)   # Tạo một ma trận rỗng có shape như x

# Thêm vector v vào mỗi hàng của ma trận x bằng một vòng lặp
for i in range(4):
    y[i, :] = x[i, :] + v

# Ma trận y sẽ như sau
# [[ 2  2  4]
#  [ 5  5  7]
#  [ 8  8 10]
#  [11 11 13]]
print(y)
  • Gọi hàm và thực hiện phép tính trực tiếp trên toàn bộ ndarray mà không cần bỏ trong vòng lặp for ..
  • Đọc/ghi memory-mapped files (lưu trữ trên bộ nhớ ảo)
  • Các phép tính nâng cao: Đại số tuyến tính, Tạo số ngẫu nhiên và Biến đổi Fourier
  • API để kết nối với thư viện viết bằng C, C++ hay FORTRAN.

NumPy mặc dù không dùng để mô hình hóa (modeling) hay ít được sử dụng để tính toán khoa học nhưng việc hiểu được "mảng NumPy" và cách thực hiện tính toán trên cấu trúc của nó sẽ giúp bạn làm việc hiệu quả hơn với các cấu trúc thừa kế bậc cao hơn, như DataFrame của thư viện pandas.

Note

📚 Dân Data cần học gì

Đối với hầu hết các ứng dụng phân tích dữ liệu, các chủ đề chính ta cần qua tâm gồm:

  • Các phép toán array-based liên quan tới "data muging""cleaning", "subetting" (phân nhóm) – "filtering" và các phép biến đổi ("transform") …
  • Các thuật toán dùng trên mảng phổ biến như sắp xếp "sort", chống trùng "unique"và các phép toán tập hợp "set operations"
  • Phân tích thống kê mô tả "descriptive statistics" và Tổng hợp dữ liệu "aggreagting data" /"summarizing data"
  • Sắp xếp dữ liệu "data alignment" và các phép biến đổi dữ liệu quan hệ "relational data manipulations" như kết hợp "merge", nối "join" các bộ dữ liệu không đồng nhất "heterogeneous datasets".
  • Các biểu thức logic điều kiện "if-elif-else"
  • Xử lý dữ liệu theo nhóm "group-wise data manipulations" (tổ hợp, biến đổi và áp hàm xử lý)

Như những buổi đầu mình có đề cập thì pandas là phần cải tiến của NumPy, đặc biệt trong khả năng xử lý những kiểu dữ liệu time series. Vậy nên hầu hết mọi người đều dùng pandas như là công cụ chính để phân tích và thông kê các dữ liệu đặc thù như tabular data hay time series data.

Tips 💡

Sự ra đời của NumPy bắt đầu từ nhu cầu tính toán trên mảng "Array-oriented computing" bằng Python từ những năm 1995, khi Jim Hugunin tạo ra thư viện Numeric.

Trong 10 năm tiếp theo, các cộng đồng khoa học bắt đầu lập trình xử lý trên mảng đa chiều nhiều hơn, nhưng hệ sinh thái thư viện khi đó khá rời rạc (vào đầu những năm 2000).

Năm 2005, Travis Oliphant đã hợp nhất và ra mắt NumPy từ các thư viện NumericNumarray làm chuẩn chung.

Fun fact

📚Lý do bạn học NumPy

Một trong những lý do khiến NumPy rất quan trọng cho các tính toán đại số trong Python là vì nó được thiết kế để có hiệu suất cao nhất khi xử lý các mảng dữ liệu lớn. Có một số lý do cho điều này:

  • NumPy bản thân nó lưu trữ dữ liệu trên “một khối bộ nhớ liền kề” – "a contiguous block of memory" (Như vậy nó sẽ không bị phân tán khi đọc và ghi).
  • Thư viện các thuật toán của NumPy được viết bằng ngôn ngữ bậc thấp C nghĩa là nó có quyền truy cập trực tiếp vào phần cứng máy tính mà không cần "Python interpreter" biên dịch ra ngôn ngữ máy.
  • ndarray sử dụng ít bộ nhớ hơn nhiều so với các sequences khác.
  • NumPy thực hiện các phép tính phức tạp trên toàn bộ mảng mà không cần sử dụng vòng for.

🎯 Test speed

Để thấy sự khác biệt hiệu năng chúng ta sẽ thử so sánh bằng phép tính trên 1 triệu số nguyên giữa ndarraylist

import numpy as np

my_arr = np.arange(1_000_000)

my_list = list(range(1_000_000))

Giờ ta nhân 2 cho mỗi sequence trên (ndarray cũng là sequence):

In [10]: %timeit my_arr2 = my_arr * 2
309 us +- 7.48 us per loop (mean +- std. dev. of 7 runs, 1000 loops each)

In [11]: %timeit my_list2 = [x * 2 for x in my_list]
46.4 ms +- 526 us per loop (mean +- std. dev. of 7 runs, 10 loops each)

Như ta thấy thì NumPy dựa trên những thuật toán của nó có thể cho ra kết quả nhanh gấp 10 tới 100 và thậm chí là sử dụng ít bộ nhớ hơn

🍀The NumPy ndarray – Mảng đa chiều

Ưu điểm thấy rõ giữa mảng đa chiều và danh sách Python (vốn là mảnh một chiều) là:

  • Chứa được đa dạng những bộ dữ liệu lớn trên cùng một đối tượng.
  • Hỗ trợ việc tính toán trên toàn bộ các chiều và khối dữ liệu với hiệu năng cao bằng các cú pháp quen thuộc.

Ví dụ:

In [12]: import numpy as np

In [13]: data = np.array([[1.5, -0.1, 3], [0, -3, 6.5]])

In [14]: data
Out[14]: 
array([[ 1.5, -0.1,  3. ],
       [ 0. , -3. ,  6.5]])

Ở trên ta tạo ra một mảng 2-chiều , đoạn sau đây sẽ cho ta thấy khả năng "batch computations" – tính toán hàng loạt của NumPy

In [15]: data * 10
Out[15]: 
array([[ 15.,  -1.,  30.],
       [  0., -30.,  65.]])

In [16]: data + data
Out[16]: 
array([[ 3. , -0.2,  6. ],
       [ 0. , -6. , 13. ]])

Ta thấy được tất cả các phần tử đều được nhân cho 10 mà không cần một vòng fornào và tiếp dưới đó là khả năng cộng hai mảng đa chiều lại với nhau với cú pháp đơn giản như tổng đại số.

Trong các ví dụ sau ta sẽ thêm thư viện bằng cú pháp import numpy as np, tất nhiên là ta có thể dùng cú pháp from numpy import * để khỏi phải gọi lặp đi lặp lại khai báo namespace np.

Nhưng các bạn không nên làm vậy vì numpy namespace chứa rất nhiều hàm giống tên với các hàm Python mặc định như min hay max

Note 📙

ndarray là một container (là một cấu trúc dữ liệu lưu trữ các đối tượng) đa chiều cho các dữ liệu đồng dạng "homogeneous data" – nghĩa là chúng phải có cùng kiểu dữ liệu trên cùng 1-chiều.

Mỗi ndarray có:

  • shape: một tuple đặc tả kích thước của mỗi chiều.
  • dtype: một đối tượng mô tả kiểu dữ liệu của mảng.
In [17]: data.shape
Out[17]: (2, 3) # 2 rows - 3 columns

In [18]: data.dtype
Out[18]: dtype('float64')

Để thống nhất thì từ bài này trở đi khi mình ghi "mảng" hay "mảng NumPy" hay "ndarray" thì chung quy lại đều đang đề cập tới đối tượng "ndarray"

Note 💡

🍀 Cách tạo ndarray

🔥 Hàm thường sử dụng

Khác với list hay các sequence object khác, ndarray có thể tạo bằng hàm array của NumPy. Hàm này có thể nhận bất kỳ sequence object nào (gồm các ndarray khác) và cho ra một NumPy array chứa những dữ liệu đã được truyền vào.

▶ Ví dụ: Convert list sau thành NumPy array (1 chiều)

In [19]: data1 = [6, 7.5, 8, 0, 1]

In [20]: arr1 = np.array(data1)

In [21]: arr1
Out[21]: array([6. , 7.5, 8. , 0. , 1. ])

❗ Lưu ý: Như ta thấy thì ở trên các phần tử được chuyển thành kiểu float hết từ hỗn hợp intfloat từ list ban đầu vì ndarray chỉ chứa homogeneous data trên cùng 1-chiều.

📑 Nested sequences

Hàm np.array() nếu nhận nested sequences thì kết quả đầu ra là mảng đa chiều:

In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]

In [23]: arr2 = np.array(data2)

In [24]: arr2
Out[24]: 
array([[1, 2, 3, 4],
           [5, 6, 7, 8]])

Trong ví dụ ở trên ta đưa vào đối tượng nested list [các danh sách con trong danh sách cha – "list of lists"] sẽ cho đầu ra sẽ là mảng NumPy 2 chiều.

Điều thú vị là hình dáng ("shape") của NumPy array được nội suy ("inferred") từ chính dữ liệu nhận vào

Mindset data-driven.

Note 📙

📑 ndim và shape

Giờ ta thử xác nhận kết luận này bằng cách kiểm tra ("inspecting") các thuộc tính của nó ndimshape

In [25]: arr2.ndim
Out[25]: 2

In [26]: arr2.shape
Out[26]: (2, 4) # 2 rows - 4 columns

📑 dtype

Tương tự "shape" trừ khi được định nghĩa trước thì numpy.array sẽ tự xác định kiểu dữ liệu phù hợp nhất cho mảng mà nó tạo ra. Kiểu dữ liệu này sẽ được lưu vào đối tượng metadata (data about data) dtype (sẽ đề cập sâu phần bên dưới).

Ví dụ:

In [27]: arr1.dtype
Out[27]: dtype('float64')

In [28]: arr2.dtype
Out[28]: dtype('int64')

🔥 Hàm khởi tạo khác

Ngoài numpy.array có rất nhiều hàm khác để tạo ra mảng mới.

🔰 numpy.zerosnumpy.ones tạo ra các mảng 0s và 1s (chữ s này dịch là "các" ví dụ 1900s) với kích thước tùy vào tham số truyền vào là length hoặc shape:

In [29]: np.zeros(10)
Out[29]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

In [30]: np.zeros((3, 6))
Out[30]: 
array([[0., 0., 0., 0., 0., 0.],
            [0., 0., 0., 0., 0., 0.],
            [0., 0., 0., 0., 0., 0.]])

🔰 numpy.empty tạo ra một mảng đặc biệt khi không cần khởi tạo "initializing" các giá trị của nó. Ta dùng tuple để khai báo mảng đa chiều

In [31]: np.empty((2, 3, 2))
Out[31]: 
array([[[0., 0.],
             [0., 0.],
             [0., 0.]],
            [[0., 0.],
             [0., 0.],
             [0., 0.]]])

Sai lầm nghiêm trọng là mọi người thường cho rằng numpy.empty sẽ trả về một mảng chứa các số 0 (ở trên do là mình đã làm tròn thôi).

Nhưng thật chất hàm này trả về vùng nhớ chưa được khởi tạo "uninitialized memory" và vì vậy đúng hơn nó chứa các giá trị nonzero "gabarge" values.

🍀 Ta sẽ dùng hàm này khi muốn tạo một mảng mới và fill up data sau.

Warning❗️❗️

🔰 numpy.arrange() là phiên bản "array-valued" của hàm range() chúng ta vẫn hay gặp:

In [32]: np.arange(15)
Out[32]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
Danh sách các hàm tạo NumPy array quan trọng

🍀Các kiểu dữ liệu của ndarrays

🔥 Python types vs NumPy types

"data type" hay dtype là đối tượng chứa thông tin (metadata) được ndarray dùng định danh "interpret" kiểu dữ liệu của giá trị tham số truyền vào:

In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64)

In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32)

In [35]: arr1.dtype
Out[35]: dtype('float64')

In [36]: arr2.dtype
Out[36]: dtype('int32')

Khả năng NumPy có thể linh hoạt tương tác với các dữ liệu đến từ các hệ thống khác nhau chính nhờ bản thân các kiểu dữ liệu đặc thù mà nó hỗ trợ.

🔰 Hầu hết trường hợp thì dtype sẽ ánh xạ cụ thể trực tiếp các kiểu dữ liệu xuống ổ cứng và bộ nhớ, nói cách khác là nó có khả năng đọc và ghi trực tiếp các luồng dữ liệu dạng nhị phân và tương thích với các dòng lệnh bậc thấp mà hệ thống sử dụng như C hay FORTRAN.

NumPy data types

🔰 Tương tự với các kiểu dữ liệu mặc định trong Python, dữ liệu dạng số cũng có các kiểu với tên tương đồng: float hay int nhưng khác biệt là theo sau nó là số bits định danh kích thước cho mỗi kiểu.

Ví dụ: Kiểu số thực tiêu chuẩn với 2 chữ số thập phân dùng trong Python chiếm 8 bytes hoặc 64 bits nên trong NumPy sẽ có kiểu tương tự là float64.

Đừng lo vì bạn không cần phải nhớ tới các kiểu dữ liệu NumPy đâu, chỉ cần nhớ tới các kiểu chung của Python như float, integer, string, complex (số phức), boolean và một vài kiểu đối tượng cơ bản (các kiểu dữ liệu cũng là đối tượng).

Chỉ khi bạn cần kiếm soát nhiều hơn về việc lưu trữ vào bộ nhớ, đặc biệt là làm việc với BigData thì lúc đó hãy hiểu rõ để giảm chi phí lưu trữ và xử lý.

Tips 💡

🔥 Signed và Unsigned integer

Nhiều bạn sẽ bối rối khi nghe 2 thuật ngữ này.

  • Số nguyên có dấu: đại diện cho "số nguyên dương""số nguyên âm" bao gồm số 0.
  • Số nguyên không dấu: đại diện cho "số nguyên không âm" gồm số 0.

Ví dụ:

  • int8 (signed 8-bit integer) có khoảng giá trị từ -128 tới 127
  • uint8 (unsigned 8-bit integer) có khoảng giá trị từ 0 tới 255

🔥 Ép kiểu một mảng

📑 Hàm astype

🔰 NumPy cung cấp cho ta hàm astype để chuyển đổi các kiểu dữ liệu khác nhau:

In [37]: arr = np.array([1, 2, 3, 4, 5])

In [38]: arr.dtype
Out[38]: dtype('int64')

In [39]: float_arr = arr.astype(np.float64)

In [40]: float_arr
Out[40]: array([1., 2., 3., 4., 5.])

In [41]: float_arr.dtype
Out[41]: dtype('float64')

🔰 Nếu dùng astype chuyển đổi từ float về ngược lại int thì phần thập phân sẽ bị xén đi (Không phải làm tròn nhé)

In [42]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])

In [43]: arr
Out[43]: array([ 3.7, -1.2, -2.6,  0.5, 12.9, 10.1])

In [44]: arr.astype(np.int32)
Out[44]: array([ 3, -1, -2,  0, 12, 10], dtype=int32)

numpy.string_ nếu số ký tự vượt khỏi giới hạn kích thước của NumPy string thì nó sẽ tự bị xén mà không cảnh báo ta.

Warning ❗❗❗

Nếu ép kiểu ("casting") lỗi thì ta sẽ nhận được lỗi ValueError.

📑 Ép kiểu gián tiếp

Ta có thể ép kiểu của mảng này bằng kiểu của mảng khác:

In [47]: int_array = np.arange(10)

In [48]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)

In [49]: int_array.astype(calibers.dtype)
Out[49]: array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

🔰 Khi khai báo kiểu cho dtype ta có thể dùng dạng rút gọn ví dụ "uint32" thành "u4" (xem bảng trên):

In [50]: zeros_uint32 = np.zeros(8, dtype="u4")

In [51]: zeros_uint32
Out[51]: array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)

Hàm astype() sẽ khởi tạo một mảng khác có kiểu dữ liệu giống với kiểu dữ liệu sao chép

Tips 💡

🍀 Các phép tính của mảng NumPy

Tính năng quan trọng của ndarray là bạn có thể thực hiện các phép toán trên toàn bộ mảng mà không cần vòng for – chức năng này gọi là "vectorization".

❗️Lưu ý: Chỉ mảng có kích thước giống nhau thì mới là "vectorization" còn khác kích thước (dimension) thì gọi là "broadcasting":

In [52]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])

In [53]: arr
Out[53]: 
array([[1., 2., 3.],
            [4., 5., 6.]])

In [54]: arr * arr
Out[54]: 
array([[ 1.,  4.,  9.],
            [16., 25., 36.]])

In [55]: arr - arr
Out[55]: 
array([[0., 0., 0.],
           [0., 0., 0.]])

Tất nhiên ta có thể tính toán trên với số học như:

In [56]: 1 / arr
Out[56]: 
array([[1.    , 0.5   , 0.3333],
           [0.25  , 0.2   , 0.1667]])

In [57]: arr ** 2
Out[57]: 
array([[ 1.,  4.,  9.],
           [16., 25., 36.]])

Các phép so sánh cũng được áp dụng với các mảng cùng kích thước, kết quả trả về sẽ là Booleans

In [58]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])

In [59]: arr2
Out[59]: 
array([[ 0.,  4.,  1.],
           [ 7.,  2., 12.]])

In [60]: arr2 > arr
Out[60]: 
array([[False,  True, False],
            [ True, False,  True]])

Khái niệm “broadcasting” (Tính toán trên các mảng khác dimensions) còn được gọi là “propagated”

Tips 💡

🍀 Indexing và Slicing cơ bản

Đối với mảng 1 chiều bạn có thể xem như nó tương đồng với danh sách Python "list":

In [61]: arr = np.arange(10)

In [62]: arr
Out[62]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [63]: arr[5]
Out[63]: 5

Và vì vậy bạn cũng có vô số cách để chọn ra phần con "subset" của mảng – Kỹ thuật này gọi là slicing hay "thái lát" mà mình đã học

In [64]: arr[5:8]
Out[64]: array([5, 6, 7])

In [65]: arr[5:8] = 12

In [66]: arr
Out[66]: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

🔥 Broadcast

Khi bạn gán arr[5:8] = 12 thì giá trị sẽ được áp cho toàn bộ phần con propagated hay "broadcast"

/In [67]: arr_slice = arr[5:8]

In [68]: arr_slice
Out[68]: array([12, 12, 12])

Khác biệt quan trọng so với Python list là phần cắt "slices" ánh xạ trực tiếp tới mảng gốc hay dữ liệu không được sao chép và bất kỳ sửa đổi nào ảnh hưởng trực tiếp tới mảng gốc.

Warning ❗❗
In [69]: arr_slice[1] = 12345

In [70]: arr
Out[70]: 
array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
           9])

🎯 Lý do cho việc này là Numpy phải thao tác trên các dữ liệu dung lượng lón nên việc giữ y xì quy tắc copy như các các ngôn ngữ lập trình mảng khác sẽ dẫn tới các vấn đề về hiệu năng và bộ nhớ

Nếu bạn muốn copy slice của ndarray thay vì chỉ là một view thì bạn cần dùng phương thức arr[5:8].copy(). Thư viện pandas cũng hoạt động giống vậy

Warning ❗❗

🔥 Mảng đa chiều

Với các mảng đa chiều bạn có nhiều tùy biến hơn. Vd mảng 2 chiều, các phần tử của mỗi chỉ mục lại là các mảng một chiều thay vì giá trị như trước:

In [73]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

In [74]: arr2d[2]
Out[74]: array([7, 8, 9])

Bạn có thể truy cập phần con của nó bằng đệ quy nhưng ngoài ra có thể sử dụng dấu , vẫn có tác dụng tương tự:

In [75]: arr2d[0][2]
Out[75]: 3

In [76]: arr2d[0, 2]
Out[76]: 3

Để dễ hiểu cho phần đánh chỉ mục "indexing" mảng 2 chiều thì mình có hình sau:

Indexing elements in a NumPy array

Đối với mảng nhiều chiều hơn (như 3 chiều), nếu bạn lấy lát của một mảng mà không chỉ định tất cả các chỉ số, bạn sẽ nhận được một mảng mới có kích thước thấp hơn, bao gồm tất cả dữ liệu dọc theo các kích thước cao hơn. Nói khó hiểu nhỉ, vào ví dụ nào:

In [77]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

In [78]: arr3d
Out[78]: 
array([[[ 1,  2,  3],
        [ 4,  5,  6]],
       [[ 7,  8,  9],
        [10, 11, 12]]])

Ở trên ta có một mảng 3 chiều 2x2x3 , đơn giản là lồng ghép 3 mảng có kích thước lần lược 2 (phần tử), 2 và 3 thôi. Và khi ta lấy index 0 ra ta lại nhận được 1 mảng 2 chiều với kích thước 2x3

In [79]: arr3d[0]
Out[79]: 
array([[1, 2, 3],
       [4, 5, 6]])

Thử tính toán trên mảng cắt này xem sao:

In [80]: old_values = arr3d[0].copy()

In [81]: arr3d[0] = 42

In [82]: arr3d
Out[82]: 
array([[[42, 42, 42],
        [42, 42, 42]],
       [[ 7,  8,  9],
        [10, 11, 12]]])

# Trả về mảng gốc
In [83]: arr3d[0] = old_values

In [84]: arr3d
Out[84]: 
array([[[ 1,  2,  3],
        [ 4,  5,  6]],
       [[ 7,  8,  9],
        [10, 11, 12]]])

Vậy nếu mình cắt theo kiểu khác thì sao? Ở đây với arr3d[1,0] ta sẽ nhận được mảng 1 chiều với bắt đầu từ trong ra ngoài (1, 0)

In [85]: arr3d[1, 0]
Out[85]: array([7, 8, 9])

Ta cũng có thể viết một cách tường minh dạng đệ quy từng bước như sau:

In [86]: x = arr3d[1]

In [87]: x
Out[87]: 
array([[ 7,  8,  9],
       [10, 11, 12]])

In [88]: x[0]
Out[88]: array([7, 8, 9])

❗ Nhớ là tất cả phần cắt nãy giờ vẫn chỉ là view mà thôi, hãy cẩn thận.

Các cú pháp của mảng đa chiều NumPy như trên không xài được với nested list Python mặc định nhé

Warning ❗❗

📑 Indexing with slices

Với mảng 1 chiều thì y xì danh sách Python:

In [89]: arr
Out[89]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

In [90]: arr[1:6]
Out[90]: array([ 1,  2,  3,  4, 64])

Ví dụ mảng 2 chiều ở trên thì có khác xíu:

In [91]: arr2d
Out[91]: 
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [92]: arr2d[:2]
Out[92]: 
array([[1, 2, 3],
       [4, 5, 6]])

Để dễ hiểu có thể phiên dịch lại biểu thức arr2[:2] là “chọn 2 dòng đầu của mảng arr2“. Nói cách khác nó đã cắt lát dọc theo trục 0, do đó một lát mảng sẽ chọn một dải các phần tử dọc theo một trục.

Thử cắt cách khác xem, lúc này arr2d[1:2] đang slice trên 1 mảng 1 chiều

arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
arr2d[1:2]

# array([[1, 2, 3],
       [4, 5, 6]])

Ta có thể truyền vào nhiều slices như cách truyền vào nhiều chỉ mục (ở phần đệ quy ở trên):

In [93]: arr2d[:2, 1:]
Out[93]: 

array([[2, 3],
       [5, 6]])

📑 Kết hợp vừa index vừa slice

Ví dụ 1: Bạn có thể chọn chỉ dòng thứ hai và hai cột đầu bằng cách sau:

In [94]: lower_dim_slice = arr2d[1, :2]

Bằng cách trên thì ta trả về một mảng 1 chiều với shape là tuple mang giá trị của 1 chiều:

In [95]: lower_dim_slice.shape
Out[95]: (2,)

Ví dụ 2: Tương tự bạn có thể chọn cột thứ 3 nhưng chỉ 2 dòng đầu:

In [96]: arr2d[:2, 2]
Out[96]: array([3, 6])

🎯 Dấu , có ý nghĩa lấy toàn bộ trục, vậy nên bạn chỉ có thể slice chiều cao hơn bằng cách sau:

In [97]: arr2d[:, :1]
Out[97]: 
array([[1],
       [4],
       [7]])

Và tất nhiên khi gán một giá trị số vào slice ta cũng có kết quả giống như trên:

In [98]: arr2d[:2, 1:] = 0

In [99]: arr2d
Out[99]: 
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])
2-dimensional array slicing

🍀 Boolean Indexing

🔥Mảng Booleans

Xem ví dụ sau ta có 2 mảng namesdata

In [100]: names = np.array(["Bob", "Joe", "Will", "Bob", "Will", "Joe", "Joe"])

In [101]: data = np.array([[4, 7], [0, 2], [-5, 6], [0, 0], [1, 2],
   .....:                  [-12, -4], [3, 4]])

In [102]: names
Out[102]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')

In [103]: data
Out[103]: 
array([[  4,   7],
       [  0,   2],
       [ -5,   6],
       [  0,   0],
       [  1,   2],
       [-12,  -4],
       [  3,   4]])

Giả sử mỗi tên tương ứng với một hàng trong mảng data và chúng ta muốn chọn tất cả các hàng có tên tương ứng là "Bob". Giống như các phép toán số học, các phép so sánh (chẳng hạn như ==) với mảng cũng được vectorized. Do đó, việc so sánh names với chuỗi "Bob" sẽ tạo ra một mảng Boolean:

In [104]: names == "Bob"
Out[104]: array([ True, False, False,  True, False, False, False])

Ở trên names == "Bob" là một mảng Booleans là mảng này có thể thay làm chỉ mục cho mảng 2 chiều data:

In [105]: data[names == "Bob"]
Out[105]: 
array([[4, 7],
       [0, 0]])

❗ Lưu ý là mảng Boolean phải cùng kích cỡ với chiều mảng nó đang đánh chỉ mục.

🔥 Kết hợp slicing

Bạn còn có thể kết hợp với slices cùng với mảng Boolean trên

In [106]: data[names == "Bob", 1:]
Out[106]: 
array([[7],
       [0]])

In [107]: data[names == "Bob", 1]
Out[107]: array([7, 0])

Để chọn tất cả các dòng khi names == "Bob" và đánh chỉ mục mỗi cột:

In [106]: data[names == "Bob", 1:] #Lấy từ index 1 trở đi
Out[106]: 
array([[7],
       [0]])

In [107]: data[names == "Bob", 1] #Lấy các gái trị cột ở vị trí True
Out[107]: array([7, 0])

Để lấy phần bù của "Bob" bạn có thể dùng != hoặc để toán tử phủ định negate~

In [108]: names != "Bob"
Out[108]: array([False,  True,  True, False,  True,  True,  True])

In [109]: ~(names == "Bob")
Out[109]: array([False,  True,  True, False,  True,  True,  True])

In [110]: data[~(names == "Bob")]
Out[110]: 
array([[  0,   2],
       [ -5,   6],
       [  1,   2],
       [-12,  -4],
       [  3,   4]])

Toán tử ~có thể hữu dụng khi bạn muốn đảo ngược mảng Boolean gán với một biến nào đó

In [111]: cond = names == "Bob"

In [112]: data[~cond]
Out[112]: 
array([[  0,   2],
       [ -5,   6],
       [  1,   2],
       [-12,  -4],
       [  3,   4]])

Để chọn hai hoặc ba tên trong mảng names bạn có thể sử dụng các toán tử Boolean như & (and) và | (or)

In [113]: mask = (names == "Bob") | (names == "Will")

In [114]: mask
Out[114]: array([ True, False,  True,  True,  True, False, False])

In [115]: data[mask]
Out[115]: 
array([[ 4,  7],
       [-5,  6],
       [ 0,  0],
       [ 1,  2]])

Điểm khác biệt lớn là khi chọn dữ liệu từ mảng bằng Boolean indexing và gán kết quả vào biến mới (2 điều kiện) thì ta đã tạo một bản copy thậm chí cả khi mảng trả về chưa thay đổi gì hết (chọn hết)

Note 📌

andor không hiệu lực với Boolean array mà là &|

Warning ❗❗

🔥 Logic slicing

Ta có thể slicing từ một điều kiện Logic như sau, miễn sao nó trả về một Boolean array là được:

In [116]: data[data < 0] = 0

In [117]: data
Out[117]: 
array([[4, 7],
       [0, 2],
       [0, 6],
       [0, 0],
       [1, 2],
       [0, 0],
       [3, 4]])

Bạn cũng có thể gán toàn bộ dòng và cột bằng cách sử dụng một mảng một chiều Boolean:

In [118]: data[names != "Joe"] = 7

In [119]: data
Out[119]: 
array([[7, 7],
       [0, 2],
       [7, 7],
       [7, 7],
       [7, 7],
       [0, 0],
       [3, 4]])

Về sau ta sẽ thấy các loại xử lý trên rất hữu dụng ở mảng 2 chiều trong pandas

🍀 Fancy indexing

Đây là kỹ thuật dùng mảng số nguyên để indexing. Ví dụ ta có 1 mảng 8×4 sau:

In [120]: arr = np.zeros((8, 4))

In [121]: for i in range(8):
   .....:     arr[i] = i

In [122]: arr
Out[122]: 
array([[0., 0., 0., 0.],
       [1., 1., 1., 1.],
       [2., 2., 2., 2.],
       [3., 3., 3., 3.],
       [4., 4., 4., 4.],
       [5., 5., 5., 5.],
       [6., 6., 6., 6.],
       [7., 7., 7., 7.]])

Để chọn tập con các dòng ở các vị trí xác định, đơn giản bạn có thể truyền vào danh sách hoặc ndarray cac số nguyên xác định các vị trí mong muốn:

In [123]: arr[[4, 3, 0, 6]]
Out[123]: 
array([[4., 4., 4., 4.],
       [3., 3., 3., 3.],
       [0., 0., 0., 0.],
       [6., 6., 6., 6.]])

Ở chiều ngược lại từ dưới lên:

In [124]: arr[[-3, -5, -7]]
Out[124]: 
array([[5., 5., 5., 5.],
       [3., 3., 3., 3.],
       [1., 1., 1., 1.]])

Vậy nếu dùng các mảng index thì sao? Nó sẽ trả về 1 mảng 1 chiều ứng với từng cặp tuple index suy từ 2 mảng indeces (index) nested đó (0 index)

In [125]: arr = np.arange(32).reshape((8, 4))

In [126]: arr
Out[126]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23],
       [24, 25, 26, 27],
       [28, 29, 30, 31]])

In [127]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[127]: array([ 4, 23, 29, 10])

Trong trường hợp này, các phần tử (1, 0), (5, 3), (7, 1), và (2, 2) đã được chọn. Kết quả của việc sử dụng fancy indexing luôn là mảng một chiều.

Trong ví dụ sau đây ta còn có thể sắp xếp lại vị trí các cột

In [128]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[128]: 
array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])

Lưu ý rằng fancy indexing, không giống như slicing, luôn sao chép dữ liệu vào một mảng mới khi gán kết quả cho một biến mới. Nếu bạn gán giá trị bằng fancy indexing, các giá trị được lập chỉ mục sẽ bị sửa đổi:

In [129]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[129]: array([ 4, 23, 29, 10])

In [130]: arr[[1, 5, 7, 2], [0, 3, 1, 2]] = 0

In [131]: arr
Out[131]: 
array([[ 0,  1,  2,  3],
       [ 0,  5,  6,  7],
       [ 8,  9,  0, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22,  0],
       [24, 25, 26, 27],
       [28,  0, 30, 31]])

🍀Chuyển vị mảng và Đổi tọa độ

Chuyển vị hay pivot là một dạng đặc biệt của việc định hình lại mảng, tương tự như vậy, nó trả về một view trên dữ liệu mà không sao chép bất cứ thứ gì. Mảng có phương thức transpose() và thuộc tính đặc biệt T để thực hiện chuyển vị:

In [132]: arr = np.arange(15).reshape((3, 5))

In [133]: arr
Out[133]: 
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])

In [134]: arr.T
Out[134]: 
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])

Ta rất hay áp dụng khi tính toán ma trận ví dụ khi tính tích trong ma trận bằng numpy.dot

In [135]: arr = np.array([[0, 1, 0], [1, 2, -2], [6, 3, 2], [-1, 0, -1], [1, 0, 1
]])

In [136]: arr
Out[136]: 
array([[ 0,  1,  0],
       [ 1,  2, -2],
       [ 6,  3,  2],
       [-1,  0, -1],
       [ 1,  0,  1]])

In [137]: np.dot(arr.T, arr)
Out[137]: 
array([[39, 20, 12],
       [20, 14,  2],
       [12,  2, 10]])

Toán tử trung tố ("infix") là cách khác để thao tác với ma trận như trên:

In [138]: arr.T @ arr
Out[138]: 
array([[39, 20, 12],
       [20, 14,  2],
       [12,  2, 10]])

Việc chuyển vị bằng .T là trường hợp đặc biệt của đổi tọa độ. ndarray có phương thức swapaxes nhận cặp các số tọa độ và chuyển đổi các chiều được chỉ định để sắp xếp lại dữ liệu:

In [139]: arr
Out[139]: 
array([[ 0,  1,  0],
       [ 1,  2, -2],
       [ 6,  3,  2],
       [-1,  0, -1],
       [ 1,  0,  1]])

In [140]: arr.swapaxes(0, 1)
Out[140]: 
array([[ 0,  1,  6, -1,  1],
       [ 1,  2,  3,  0,  0],
       [ 0, -2,  2, -1,  1]])

Và hàm swapaxes chỉ đơn giản trả về view chứ không tạo bản sao chép

🍀 Lời kết

Có lẽ phần trừu tượng nhất của NumPy cơ bản là cách các bạn Indexing và Slicing vào từng chiều của ndarray. May mắn thay như mình có hướng dẫn trong buổi Livestream chúng ta chỉ cần hình tượng hóa nó như những lớp vỏ và khi đó mọi chuyện hóa ra lại đơn giản hơn rất nhiều

Các kênh cộng đồng của mình

  • Facebook: https://www.facebook.com/bocuoilamdata
  • Youtube: https://www.youtube.com/@CarrY4U_VN
  • Website: https://carry4u.org/

© Bản quyền thuộc về CarrY4U và Bò Cười Làm Data
© Copyright by CarrY4U and bocuoilamdata ☞ Do not Reup”