06/04/2021, 14:46

Structured Array và RecordArrays trong NumPy - ài liệu học Numpy từ cơ bản đến nâng cao

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. 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 ...

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.

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 đó.

Đầ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:

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.

Bài tiếp

Trần Trung Dũng

15 chủ đề

2610 bài viết

0