NumPy之:結構化陣列詳解

flydean發表於2021-05-06

簡介

普通的陣列就是陣列中存放了同一型別的物件。而結構化陣列是指陣列中存放不同物件的格式。

今天我們來詳細探討一下NumPy中的結構化陣列。

結構化陣列中的欄位field

因為結構化陣列中包含了不同型別的物件,所以每一個物件型別都被稱為一個field。

每個field都有3部分,分別是:string型別的name,任何有效dtype型別的type,還有一個可選的title

看一個使用filed構建dtype的例子:

In [165]: np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f4')])
Out[165]: dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f4')])

我們可以使用上面的dtype型別來構建一個新的陣列:

In [166]: x = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
     ...:     dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f4')])
     ...:

In [167]: x
Out[167]:
array([('Rex', 9, 81.), ('Fido', 3, 27.)],
      dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f4')])

x是一個1維陣列,每個元素都包含三個欄位,name,age和weight。並且分別指定了他們的資料型別。

可以通過index來訪問一行資料:

In [168]: x[1]
Out[168]: ('Fido', 3, 27.)

也可以通過name來訪問一列資料 :

In [170]: x['name']
Out[170]: array(['Rex', 'Fido'], dtype='<U10')

還可以給所有的列統一賦值:

In [171]: x['age']
Out[171]: array([9, 3], dtype=int32)

In [172]: x['age'] = 10

In [173]: x
Out[173]:
array([('Rex', 10, 81.), ('Fido', 10, 27.)],
      dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f4')])

結構化資料型別

上面的例子讓我們對結構化資料型別有了一個基本的認識。結構化資料型別就是一系列的filed的集合。

建立結構化資料型別

結構化資料型別是從基礎型別建立的,主要有下面幾種方式:

從元組建立

每個元組都是(fieldname, datatype, shape)這樣的格式,其中shape 是可選的。fieldname 是 field的title。

In [174]: np.dtype([('x', 'f4'), ('y', np.float32), ('z', 'f4', (2, 2))])
Out[174]: dtype([('x', '<f4'), ('y', '<f4'), ('z', '<f4', (2, 2))])

如果fieldname是空字元的話,會以f開頭的形式預設建立。

In [177]: np.dtype([('x', 'f4'), ('', 'i4'), ('z', 'i8')])
Out[177]: dtype([('x', '<f4'), ('f1', '<i4'), ('z', '<i8')])

從逗號分割的dtype建立

可以選擇從逗號分割的dtype型別建立:

In [178]: np.dtype('i8, f4, S3')
Out[178]: dtype([('f0', '<i8'), ('f1', '<f4'), ('f2', 'S3')])

In [179]: np.dtype('3int8, float32, (2, 3)float64')
Out[179]: dtype([('f0', 'i1', (3,)), ('f1', '<f4'), ('f2', '<f8', (2, 3))])

從字典建立

從字典建立是這樣的格式: {'names': ..., 'formats': ..., 'offsets': ..., 'titles': ..., 'itemsize': ...}

這種寫法可以指定name列表和formats列表。

offsets 指的是每個欄位的byte offsets。titles 是欄位的title,itemsize 是整個dtype的size。

In [180]: np.dtype({'names': ['col1', 'col2'], 'formats': ['i4', 'f4']})
Out[180]: dtype([('col1', '<i4'), ('col2', '<f4')])

In [181]: np.dtype({'names': ['col1', 'col2'],
     ...: ...           'formats': ['i4', 'f4'],
     ...: ...           'offsets': [0, 4],
     ...: ...           'itemsize': 12})
     ...:
Out[181]: dtype({'names':['col1','col2'], 'formats':['<i4','<f4'], 'offsets':[0,4], 'itemsize':12})

操作結構化資料型別

可以通過dtype 的 names 和fields 欄位來訪問結構化資料型別的屬性:

>>> d = np.dtype([('x', 'i8'), ('y', 'f4')])
>>> d.names
('x', 'y')
>>> d.fields
mappingproxy({'x': (dtype('int64'), 0), 'y': (dtype('float32'), 8)})

Offsets 和Alignment

對於結構化型別來說,因為一個dtype中包含了多種資料型別,預設情況下這些資料型別是不對齊的。

我們可以通過下面的例子來看一下各個型別的偏移量:

>>> def print_offsets(d):
...     print("offsets:", [d.fields[name][1] for name in d.names])
...     print("itemsize:", d.itemsize)
>>> print_offsets(np.dtype('u1, u1, i4, u1, i8, u2'))
offsets: [0, 1, 2, 6, 7, 15]
itemsize: 17

如果在建立dtype型別的時候,指定了align=True,那麼這些型別之間可能會按照C-struct的結構進行對齊。

對齊的好處就是可以提升處理效率。我們看一個對齊的例子:

>>> print_offsets(np.dtype('u1, u1, i4, u1, i8, u2', align=True))
offsets: [0, 1, 4, 8, 16, 24]
itemsize: 32

Field Titles

每個Filed除了name之外,還可以包含title。

有兩種方式來指定title,第一種方式:

In [182]: np.dtype([(('my title', 'name'), 'f4')])
Out[182]: dtype([(('my title', 'name'), '<f4')])

第二種方式:

In [183]: np.dtype({'name': ('i4', 0, 'my title')})
Out[183]: dtype([(('my title', 'name'), '<i4')])

看一下fields的結構:

In [187]: d.fields
Out[187]:
mappingproxy({'my title': (dtype('float32'), 0, 'my title'),
              'name': (dtype('float32'), 0, 'my title')})

結構化陣列

從結構化資料型別建立結構化陣列之後,我們就可以對結構化陣列進行操作了。

賦值

我們可以從元組中對結構化陣列進行賦值:

>>> x = np.array([(1, 2, 3), (4, 5, 6)], dtype='i8, f4, f8')
>>> x[1] = (7, 8, 9)
>>> x
array([(1, 2., 3.), (7, 8., 9.)],
     dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '<f8')])

還可以從標量對結構化陣列進行賦值:

>>> x = np.zeros(2, dtype='i8, f4, ?, S1')
>>> x[:] = 3
>>> x
array([(3, 3., True, b'3'), (3, 3., True, b'3')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])
>>> x[:] = np.arange(2)
>>> x
array([(0, 0., False, b'0'), (1, 1., True, b'1')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])

結構化陣列還可以賦值給非機構化陣列,但是前提是結構化陣列只有一個filed:

>>> twofield = np.zeros(2, dtype=[('A', 'i4'), ('B', 'i4')])
>>> onefield = np.zeros(2, dtype=[('A', 'i4')])
>>> nostruct = np.zeros(2, dtype='i4')
>>> nostruct[:] = twofield
Traceback (most recent call last):
...
TypeError: Cannot cast array data from dtype([('A', '<i4'), ('B', '<i4')]) to dtype('int32') according to the rule 'unsafe'

結構化陣列還可以互相賦值:

>>> a = np.zeros(3, dtype=[('a', 'i8'), ('b', 'f4'), ('c', 'S3')])
>>> b = np.ones(3, dtype=[('x', 'f4'), ('y', 'S3'), ('z', 'O')])
>>> b[:] = a
>>> b
array([(0., b'0.0', b''), (0., b'0.0', b''), (0., b'0.0', b'')],
      dtype=[('x', '<f4'), ('y', 'S3'), ('z', 'O')])

訪問結構化陣列

之前講到了,可以通過filed的名字來訪問和修改一列資料:

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

返回的數值是原始陣列的一個檢視,他們是共享記憶體空間的,所以修改檢視同時也會修改原資料。

看一個filed是多維陣列的情況:

In [188]: np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
Out[188]:
array([[(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]),
        (0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])],
       [(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]),
        (0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])]],
      dtype=[('a', '<i4'), ('b', '<f8', (3, 3))])

上面構建了一個2 * 2 的矩陣,這個矩陣中的第一列是int型別,第二列是一個3 * 3 的float矩陣。

我們可以這樣來檢視各個列的shape值:

>>> x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
>>> x['a'].shape
(2, 2)
>>> x['b'].shape
(2, 2, 3, 3)

除了單列的訪問之外,我們還可以一次訪問多列資料:

>>> a = np.zeros(3, dtype=[('a', 'i4'), ('b', 'i4'), ('c', 'f4')])
>>> a[['a', 'c']]
array([(0, 0.), (0, 0.), (0, 0.)],
     dtype={'names':['a','c'], 'formats':['<i4','<f4'], 'offsets':[0,8], 'itemsize':12})

多列同時賦值:

>>> a[['a', 'c']] = (2, 3)
>>> a
array([(2, 0, 3.), (2, 0, 3.), (2, 0, 3.)],
      dtype=[('a', '<i4'), ('b', '<i4'), ('c', '<f4')])

簡單的交換列的資料:

>>> a[['a', 'c']] = a[['c', 'a']]

Record Arrays

結構化陣列只能通過index來訪問,很不方便,為此NumPy提供了一個多維陣列的子類 numpy.recarray, 然後可以通過屬性來訪問。

我們來看幾個例子:

>>> recordarr = np.rec.array([(1, 2., 'Hello'), (2, 3., "World")],
...                    dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')])
>>> recordarr.bar
array([ 2.,  3.], dtype=float32)
>>> recordarr[1:2]
rec.array([(2, 3., b'World')],
      dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])
>>> recordarr[1:2].foo
array([2], dtype=int32)
>>> recordarr.foo[1:2]
array([2], dtype=int32)
>>> recordarr[1].baz
b'World'

recarray返回的結果是一個rec.array。除了使用np.rec.array來建立之外,還可以使用view:

In [190]: arr = np.array([(1, 2., 'Hello'), (2, 3., "World")],
     ...: ...                dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'a10')])
     ...:

In [191]: arr
Out[191]:
array([(1, 2., b'Hello'), (2, 3., b'World')],
      dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])

In [192]: arr.view(dtype=np.dtype((np.record, arr.dtype)),
     ...: ...                      type=np.recarray)
     ...:
Out[192]:
rec.array([(1, 2., b'Hello'), (2, 3., b'World')],
          dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])

如果是rec.array物件,它的dtype型別會被自動轉換成為np.record型別:

In [200]: recordarr.dtype
Out[200]: dtype((numpy.record, [('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')]))

想要轉換回原始的np.ndarray型別可以這樣:

In [202]: recordarr.view(recordarr.dtype.fields or recordarr.dtype, np.ndarray)
Out[202]:
array([(1, 2., b'Hello'), (2, 3., b'World')],
      dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])

如果通過index或者field來訪問rec.array物件的欄位,如果欄位是結構型別,那麼會返回numpy.recarray,如果是非結構型別,則會返回numpy.ndarray:

>>> recordarr = np.rec.array([('Hello', (1, 2)), ("World", (3, 4))],
...                 dtype=[('foo', 'S6'),('bar', [('A', int), ('B', int)])])
>>> type(recordarr.foo)
<class 'numpy.ndarray'>
>>> type(recordarr.bar)
<class 'numpy.recarray'>

本文已收錄於 http://www.flydean.com/05-python-structured-arrays/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章