NUMPY
CÁC CHỦ ĐỀ
BÀI MỚI NHẤT
MỚI CẬP NHẬT

Thông báo: Download 4 khóa học Python từ cơ bản đến nâng cao tại đây.

Structured Array và RecordArrays trong NumPy

Vậy là chúng ta đã đến bài cuối cùng của chương NumPy, khi bạn học đến đây thì nhìn chung bạn đã nắm được bao quát được cơ bản về NumPy. Trong bài cuối này, mình sẽ nói đến dữ liệu có cấu trúc trong NumPy.

test php

banquyen png
Bài viết này được đăng tại freetuts.net, không được copy dưới mọi hình thức.

Từ bài 1 đến bài 7, ta chỉ sử dụng mảng có một kiểu dữ liệu duy nhất, tuy nhiên sẽ có lúc một mảng cần chứa nhiều kiểu dữ liệu khác nhau để tạo sự liên kết trong dữ liệu. Ta hoàn toàn có thể làm điều đó trong NumPy và bài này sẽ giúp chúng ta làm việc đó.

1. Structured Arrays

Giới thiệu về Structured Arrays

Giả sử ta có các mảng chứa dữ liệu của một gia đình (tên, tuổi, chiều cao) sau:

In[2]
name = ['Minh', 'Lan', 'Linh', 'Thanh', 'Ngoc']
age = [22, 40, 21, 19, 50]
height = [181.3, 160.6, 163.2, 175.5, 165.0]

Nhìn cũng ổn, tuy nhiên không có bất cứ thứ gì cho ta biết rằng mấy mảng này liên quan đến nhau dù bản chất nó có quan hệ với nhau. Với NumPy, ta hoàn toàn có thể làm điều đó.

Bài viết này được đăng tại [free tuts .net]

Đầu tiên ta sẽ tạo nên một mảng khai báo các kiểu dữ liệu nằm trong đó:

In[3]
family = np.zeros(5, dtype={'names': ('name', 'age', 'height'), 'formats': ('U10', 'i4', 'f8')})
print(family.dtype)
Out[3]
[('name', '<U10'), ('age', '<i4'), ('height', '<f8')]

Vậy là ta đã khai báo một mảng dữ liệu có cấu trúc với các kiểu dữ liệu:

  • U10: chuỗi Unicode có độ dài tối đa là 10
  • i4: 4-byte (32-bit) integer
  • f8: 8-byte (64-bit) float

Vì đây là mảng rỗng, nên ta sẽ gán các dữ liệu ở trên vào:

In[4]
family['name'] = name
family['age'] = age
family['height'] = height
print(family)
Out[4]
[('Minh', 22, 181.3) ('Lan', 40, 160.6) ('Linh', 21, 163.2)
 ('Thanh', 19, 175.5) ('Ngoc', 50, 165. )]

Vậy là ta đã tạo thành công một structured array đơn giản, trong mục tiếp chúng ta sẽ tìm hiểu các cách để khởi tạo nên nó.

Khởi tạo Structured Arrays

Đầu tiên ta có bảng các kiểu dữ liệu:

Ký hiệu Kiểu dữ liệu tương ứng Ví dụ
'b' Byte np.dtype('b')
'i' Signed Integer np.dtype('i4') == np.int32
'u' Unsigned Integer np.dtype('u1') == np.uint8
'f' Floating point np.dtype('f8') == np.float64
'c' Complex floating point np.dtype('c16') == np.complex128
'S', 'a' String np.dtype('S')
'U' Unicode string np.dtype('U') == np.str_
'V' Raw data (void) np.dtype('S') == np.void

Có khá là nhiều kiểu tạo structured array, như ví dụ ở trên ta có:

In[5]
np.dtype({'names': ('name', 'age', 'height'), 'formats': ('U10', 'i4', 'f8')})
Out[5]
dtype([('name', '<U10'), ('age', '<i4'), ('height', '<f8')])

Ngoài ra ta có thể truyền thẳng một List vào như sau:

In[6]
np.dtype([('name', 'U10'), ('age', 'i4'), ('height', 'f8')])
Out[6]
dtype([('name', '<U10'), ('age', '<i4'), ('height', '<f8')])

Hoặc nếu bạn không quan tâm đến tên mà chỉ cần kiểu dữ liệu thì có thể viết gọn như sau:

In[7]
np.dtype('U10,i4,f8')
Out[7]
dtype([('f0', '<U10'), ('f1', '<i4'), ('f2', '<f8')])

Truy cập và gán dữ liệu trong Structured Arrays

Truy cập dữ liệu (indexing)

Chúng ta có thể truy cập vào từng trường (field) của mảng bằng cách điền tham số là tên của field:

In[8]
x = np.array([(1, 2), (3, 4)], dtype=[('foo', 'i8'), ('bar', 'f4')])

print(x['foo'])
Out[8]
[1 3]

Hoặc là ta có thể truy cập mảng như một mảng nhiều chiều bình thường:

In[9]
print("Family: ", family)

# Lấy cột đầu tiên:
print("Cột đầu tiên: ", family[0])

# Lấy phần tử cuối cùng của field "height"
print("Phần tử cuối cùng của field height: ", family[-1]['height'])
Out[9]
Family:  [('Minh', 22, 181.3) ('Lan', 40, 160.6) ('Linh', 21, 163.2)
 ('Thanh', 19, 175.5) ('Ngoc', 50, 165. )]
Cột đầu tiên:  ('Minh', 22, 181.3)
Phần tử cuối cùng của field height:  165.0

Và tất nhiên là ta có thể sử dụng Masks cũng như Fancy Indexing cho những thao tác phức tạp hơn:

In[10]
# MASKS

# Lấy tên của những người cao hơn 170cm
print("Chiều cao > 170cm: ", family[family['height'] > 170]['name'])

# Lấy tên của những người dưới 35 tuổi
print("Dưới 35 tuổi: ", family[family['age'] < 35]['name'])

# FANCY INDEXING

# Lấy 2 field "name" và "age"
ind = ['name', 'age']
print("Tên và tuổi: ", family[ind])
Out[10]
Chiều cao > 170cm:  ['Minh' 'Thanh']
Dưới 35 tuổi:  ['Minh' 'Linh' 'Thanh']
Tên và tuổi:  [('Minh', 22) ('Lan', 40) ('Linh', 21) ('Thanh', 19) ('Ngoc', 50)]

Gán dữ liệu (assignment)

Có khá nhiều cách để gán dữ liệu cho một structured array. Cách đơn giản nhất chính là sử dụng kiểu gán truyền thống:

In[11]
x = np.array([(7, 8, 9), (4, 5, 6)], dtype='i8, f4, f8')
x[1] = (1, 2, 3)

x
Out[11]
array([(7, 8., 9.), (1, 2., 3.)],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '<f8')])

Hoặc ta có thể sử dụng Array Slicing để gán cho nhiều phần tử:

In[12]
x = np.array([(7, 8, 9), (4, 5, 6)], dtype='i8, f4, f8')

print("x = ", x)

x[:] = 0

print("x = ", x)
Out[12]
x =  [(7, 8., 9.) (4, 5., 6.)]
x =  [(0, 0., 0.) (0, 0., 0.)]

Tương tự với Fancy Indexing:

In[13]
data = np.zeros(5, dtype=[('a', 'i8'), ('b', 'f8'), ('c', 'S10')])

ind = ['a', 'c']

print("Trước: ")
print(data['a'])
print(data['c'])

data[ind] = 1

print("Sau: ")
print(data['a'])
print(data['c'])
Out[13]
Trước: 
[0 0 0 0 0]
[b'' b'' b'' b'' b'']
Sau: 
[1 1 1 1 1]
[b'1' b'1' b'1' b'1' b'1']

Structured array cũng có thể được gán cho nhau, khi đó kiểu dữ liệu của mảng bị gán sẽ bị ép sang mảng được gán và thứ tự gán cho các trường dữ liệu là thứ tự trong mảng (trường đầu tiên cho trường đầu tiên, thứ hai, thứ ba,... ) bất kể có cùng hay khác tên:

In[14]
a = np.zeros(3, dtype=[('a', 'i8'), ('b', 'f8'), ('c', 'S10')])
b = np.ones(3, dtype=[('c', 'f4'), ('y', 'S10'), ('z', 'O')])

print("a = ", a)
print("b = ", b)

b[:] = a

print("b = ", b)
Out[14]
a =  [(0, 0., b'') (0, 0., b'') (0, 0., b'')]
b =  [(1., b'1', 1) (1., b'1', 1) (1., b'1', 1)]
b =  [(0., b'0.0', b'') (0., b'0.0', b'') (0., b'0.0', b'')]

So sánh 2 Structured Arrays

Chúng ta hoàn toàn có thể so sánh 2 structure arrays với nhau, 2 mảng nếu cùng tên trường, cùng kiểu dữ liệu và cùng giá trị trong trường đó thì sẽ bằng nhau (không xét thứ tự mà xét các trường cùng tên), giá trị trả về sẽ là một mảng boolean với số lượng phần tử bằng số trường tương ứng, chẳng hạn:

In[15]
a = np.zeros(2, dtype=[('a', 'i4'), ('b', 'i4')])
b = np.zeros(2, dtype=[('a', 'i4'), ('b', 'i4')])

print("a = ", a)
print("b =  ", b)

print("a == b :", a == b)
Out[15]
a =  [(0, 0) (0, 0)]
b =   [(0, 0) (0, 0)]
a == b : [ True  True]

Nếu như khác tên trường thì sẽ hiện ra một cảnh báo:

In[16]
a = np.zeros(2, dtype=[('a', 'i4'), ('b', 'i4')])
b = np.zeros(2, dtype=[('a', 'i4'), ('c', 'i4')])

print("a = ", a)
print("b =  ", b)

print("a == b :", a == b)
Out[15]
a =  [(0, 0) (0, 0)]
b =   [(0, 0) (0, 0)]
a == b : False

<ipython-input-38-02d059bb33a9>:7: FutureWarning: elementwise == comparison failed and returning scalar instead; this will raise an error or perform elementwise comparison in the future.
  print("a == b :", a == b)

Nếu như hai mảng cùng số trường nhưng khác giá trị:

In[16]
a = np.zeros(5, dtype=[('a', 'i4'), ('b', 'i4')])
b = np.ones(5, dtype=[('a', 'i4'), ('b', 'i4')])

print("a = ", a)
print("b =  ", b)

print("a == b :", a == b)
Out[16]
a =  [(0, 0) (0, 0) (0, 0) (0, 0) (0, 0)]
b =   [(1, 1) (1, 1) (1, 1) (1, 1) (1, 1)]
a == b : [False False False False False]

2. RecordArrays

Giới thiệu về RecordArrays

NumPy cung cấp cho chúng ta một module là RecordArrays để làm việc với mảng dữ liệu có cấu trúc. Giống như MaskedArrays trong bài Masks giúp ta thao tác tiện dụng hơn, RecordArrays cho phép ta làm việc với structured array một cách hiệu quả hơn cũng như cho phép ta truy cập các field như là một thuộc tính thông qua class np.recarray

Quay lại với ví dụ mở đầu về Structured Arrays, nếu muốn lấy tên các thành viên trong gia đình thì ta sẽ làm như sau:

family['name'] # Out: array(['Minh', 'Lan', 'Linh', 'Thanh', 'Ngoc'], dtype='<U10')

Chúng ta có thể chuyển sang mảng trên sang Record Array bằng hàm np.rec.array:

In[18]
family_recarr = np.rec.array(family)

print("Family Record Array: ", family_recarr.dtype)

# Truy cập trực tiếp trường như là một thuộc tính
print("Family name: ", family_recarr.name)
Out[18]
Family Record Array:  (numpy.record, [('name', '<U10'), ('age', '<i4'), ('height', '<f8')])
Family name:  ['Minh' 'Lan' 'Linh' 'Thanh' 'Ngoc']

Ngoài ra chúng ta có thể sử dụng phương thức view trực tiếp trên mảng để chuyển sang Record Array như sau:

In[19]
family_recarr_view = family.view(np.recarray)
print(family_recarr_view.dtype)
Out[19]
(numpy.record, [('name', '<U10'), ('age', '<i4'), ('height', '<f8')])

Một số helper function hữu ích

RecordArrays đi kèm với khá nhiều helper function hữu ích, mình sẽ liệt kê một số helper function thường dùng dưới đây. Trước tiên thì chúng ta cần phải import thư viện recfunctions vào đã, ta có thể làm như sau:

from numpy.lib import recfunctions as rfn

Lưu ý là các ở các hàm sau thì sẽ nếu tham số ở hàm nào trùng tên với hàm khác thì mình sẽ chỉ chú thích một lần do đều có tác dụng như nhau.

rfn.append_fields

Đây là một hàm giúp thêm 1 trường mới vào mảng sẵn có. Cú pháp của hàm như sau:

numpy.lib.recfunctions.append_fields(base, names, data, dtypes=None, fill_value=-1, usemask=True, asrecarray=False)
  • base: Mảng cần mở rộng
  • names: Tên trường
  • data: Dữ liệu của trường
  • dtypes: Kiểu dữ liệu của trường, nếu không điền thì sẽ tự động định dạng
  • fill_value: Giá trị được dùng để điền dữ liệu nếu dữ liệu trong trường bị thiếu
  • usemask: Có trả về MaskedArray hay không
  • asrecarray: Có trả về RecordArrays hay không

Ta sẽ lấy ví dụ về gia đình đầu tiên, giả sử ta muốn thêm một trường chứa dữ liệu cân nặng vào mảng sẵn có:

In[21]
print("Family: ", family)

rfn.append_fields(family, 'weight', [75, 50]) #Chỉ thêm dữ liệu cho 2 người đầu
Out[21]
Family:  [('Minh', 22, 181.3) ('Lan', 40, 160.6) ('Linh', 21, 163.2)
 ('Thanh', 19, 175.5) ('Ngoc', 50, 165. )]

masked_array(data=[('Minh', 22, 181.3, 75), ('Lan', 40, 160.6, 50),
                   ('Linh', 21, 163.2, --), ('Thanh', 19, 175.5, --),
                   ('Ngoc', 50, 165.0, --)],
             mask=[(False, False, False, False),
                   (False, False, False, False),
                   (False, False, False,  True),
                   (False, False, False,  True),
                   (False, False, False,  True)],
       fill_value=('N/A', 999999, 1.e+20, 999999),
            dtype=[('name', '<U10'), ('age', '<i4'), ('height', '<f8'), ('weight', '<i4')])

Như ta thấy, mảng trả về không phải là một RecordArrays mà là một MaskedArray (đã nói ở bài 6), các giá trị còn thiếu (cân nặng của 3 người còn lại) sẽ được điền "--" khá quen thuộc đúng không. Nếu như ta đặt tham số usemask=False thì nếu như các phần tử bị khuyết sẽ được điền bởi fill_value mà ta set ban đầu (hoặc là giá trị mặc định):

In[22]
rfn.append_fields(family, 'weight', [75, 50], usemask=False)
Out[22]
array([('Minh', 22, 181.3,     75), ('Lan', 40, 160.6,     50),
       ('Linh', 21, 163.2, 999999), ('Thanh', 19, 175.5, 999999),
       ('Ngoc', 50, 165. , 999999)],
      dtype=[('name', '<U10'), ('age', '<i4'), ('height', '<f8'), ('weight', '<i4')])

Ta có thể thêm nhiều trường vào cùng một lúc, chẳng hạn:

In[23]
rfn.append_fields(family, ['weight', 'sex'], [[75, 50], ['m', 'f', 'f', 'm', 'm']], usemask=False)
Out[23]
array([('Minh', 22, 181.3, 75, 'm'), ('Lan', 40, 160.6, 50, 'f'),
       ('Linh', 21, 163.2, -1, 'f'), ('Thanh', 19, 175.5, -1, 'm'),
       ('Ngoc', 50, 165. , -1, 'm')],
      dtype=[('name', '<U10'), ('age', '<i4'), ('height', '<f8'), ('weight', '<i4'), ('sex', '<U1')])

rfn.drop_fields

Hàm này loại bỏ các trường được chỉ định và trả về một mảng mới. Cú pháp của hàm:

numpy.lib.recfunctions.drop_fields(base, drop_names, usemask=True, asrecarray=False)
  • base: Mảng cần loại bỏ
  • drop_names: Tên trường cần bỏ

Ví dụ ta muốn bỏ trường "age" trong mảng family:

In[24]
rfn.drop_fields(base=family, drop_names='age')
Out[24]
array([('Minh', 181.3), ('Lan', 160.6), ('Linh', 163.2), ('Thanh', 175.5),
       ('Ngoc', 165. )], dtype=[('name', '<U10'), ('height', '<f8')])

Ta cũng có thể bỏ đi nhiều trường:

In[25]
rfn.drop_fields(base=family, drop_names=['age', 'height'])
Out[25]
array([('Minh',), ('Lan',), ('Linh',), ('Thanh',), ('Ngoc',)],
      dtype=[('name', '<U10')])

rfn.rename_fields

Hàm này thay đổi tên các trường được chỉ định. Cú pháp:

numpy.lib.recfunctions.rename_fields(base, namemapper)
  • base: Mảng cần đổi
  • namemapper: Có kiểu dữ liệu dictionary chứa tên cũ và tên mới của trường tương đương

Ví dụ ta muốn đổi tên các trường sang tiếng Việt:

In[26]
rfn.rename_fields(base=family, namemapper={'age': 'Tuổi', 'name': 'Tên', 'height': 'Chiều cao'})
Out[26]
array([('Minh', 22, 181.3), ('Lan', 40, 160.6), ('Linh', 21, 163.2),
       ('Thanh', 19, 175.5), ('Ngoc', 50, 165. )],
      dtype=[('Tên', '<U10'), ('Tuổi', '<i4'), ('Chiều cao', '<f8')])

Trên là những helper function phổ biến trong RecordArrays, còn khá nhiều helper nữa mà nếu bạn muốn tìm hiểu thì có thể truy cập tại đây: Structured arrays — NumPy v1.19 Manual

3. Tổng kết

Bài này là một bài khá quan trọng, ta đã biết thêm là NumPy có hỗ trợ không chỉ kiểu dữ liệu đồng nhất mà còn nhiều kiểu dữ liệu trong một mảng, nhưng vẫn giữ được tốc độ rất nhanh.

Đây cũng là bài kết thúc Series NumPy cơ bản, nhìn chung thì sau khi hoàn thành series này, bạn đã có vốn kiến thức khá ổn về NumPy và sẵn sàng làm việc với nó. Nếu bạn chưa học Pandas và Matplotlib thì hay học thêm 2 thư viện này nhé, vì bộ ba này tạo thành 3 thư viện không thể không có trong Data Science khi làm việc với Python. Cảm ơn các bạn đã theo dõi Series này.

Cùng chuyên mục:

Cách thêm Progress Bar trong Python với chỉ một dòng Code

Cách thêm Progress Bar trong Python với chỉ một dòng Code

Toán tử Walrus Operator- Tính năng mới trong Python 3.8

Toán tử Walrus Operator- Tính năng mới trong Python 3.8

Cách nạp dữ liệu Machine Learning từ File trong Python

Cách nạp dữ liệu Machine Learning từ File trong Python

Hướng dẫn sử dụng Google Sheets API với Python

Hướng dẫn sử dụng Google Sheets API với Python

Xây dựng  web Python tự động hóa Twitter | Flask, Heroku, Twitter API & Google Sheets API

Xây dựng web Python tự động hóa Twitter | Flask, Heroku, Twitter API & Google Sheets API

Xây dựng Web Machine Learning đẹp mắt với Streamlit và Scikit-learn trong Python

Xây dựng Web Machine Learning đẹp mắt với Streamlit và Scikit-learn trong Python

Hướng dẫn tạo Chatbot đơn giản bằng PyTorch

Hướng dẫn tạo Chatbot đơn giản bằng PyTorch

11 mẹo và thủ thuật để viết Code Python hiệu quả hơn

11 mẹo và thủ thuật để viết Code Python hiệu quả hơn

Hướng dẫn làm ứng dụng TODO với Flask dành cho người mới bắt đầu trong Python

Hướng dẫn làm ứng dụng TODO với Flask dành cho người mới bắt đầu trong Python

Hướng dẫn viết Snake Game bằng Python

Hướng dẫn viết Snake Game bằng Python

Cách sử dụng chế độ interactive trong Python

Cách sử dụng chế độ interactive trong Python

Cách sử dụng Python Debugger với hàm breakpoint()

Cách sử dụng Python Debugger với hàm breakpoint()

Xây dựng ứng dụng Web Style Transfer với PyTorch và Streamlit

Xây dựng ứng dụng Web Style Transfer với PyTorch và Streamlit

Cách cài đặt Jupyter Notebook trong môi trường Conda và thêm Kernel

Cách cài đặt Jupyter Notebook trong môi trường Conda và thêm Kernel

Hướng dẫn xây dựng ứng dụng dự đoán giá cổ phiếu bằng Python

Hướng dẫn xây dựng ứng dụng dự đoán giá cổ phiếu bằng Python

Hướng dẫn tạo ứng dụng AI hội thoại với NVIDIA Jarvis trong Python

Hướng dẫn tạo ứng dụng AI hội thoại với NVIDIA Jarvis trong Python

Hỗ trợ Async trong Django 3.1

Hỗ trợ Async trong Django 3.1

8 mẹo tái cấu trúc Python giúp mã sạch hơn và Pythonic

8 mẹo tái cấu trúc Python giúp mã sạch hơn và Pythonic

Ý nghĩa của if __name__ ==

Ý nghĩa của if __name__ == "__main__" trong Python

Cách xóa phần tử trong danh sách Python

Cách xóa phần tử trong danh sách Python

Top