1. 程式人生 > >資料科學 IPython 筆記本 9.11 結構化資料:NumPy 的結構化陣列

資料科學 IPython 筆記本 9.11 結構化資料:NumPy 的結構化陣列

9.11 結構化資料:NumPy 的結構化陣列

本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。

譯者:飛龍

協議:CC BY-NC-SA 4.0

雖然我們的資料通常可以通過同構陣列來很好地表示,但有時並非如此。 本節演示了 NumPy 結構化陣列和記錄陣列的用法,它們為複合異構資料提供了有效的儲存。 雖然這裡展示的模式對於簡單操作很有用,但像這樣的場景通常適合使用 Pandas Dataframe,我們將在第三章中探索。

import numpy as np

想象一下,我們有很多人的多個數據類別(比如姓名,年齡和體重),我們希望儲存這些值以便在 Python 程式中使用。可以將它們儲存在三個獨立的陣列中:

name = ['Alice', 'Bob', 'Cathy', 'Doug']
age = [25, 45, 37, 19]
weight = [55.0, 85.5, 68.0, 61.5]

但這有點笨拙。 這裡沒有任何東西告訴我們三個陣列是相關的;如果我們可以使用單一結構來儲存所有這些資料,那將更自然。NumPy 可以使用結構化陣列處理這個問題,結構化陣列是具有複合資料型別的陣列。

回想一下,之前我們使用這樣的表示式建立了一個簡單的陣列:

x = np.zeros(4, dtype=int)

我們可以使用複合資料型別規範,以相似方式建立結構化陣列:

# 使用結構化陣列的複合資料型別
data = np.zeros(4, dtype={'names':('name', 'age', 'weight'), 'formats':('U10', 'i4', 'f8')}) print(data.dtype) # [('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]

這裡'U10'表示“最大長度為 10 的 Unicode 字串”,'i4'表示 4 位元組(即 32 位)整數,'f8'表示 8 位元組(即 64 位)浮點數。我們將在下一節中討論這些型別程式碼的其他選項。

現在我們已經建立了一個空的容器陣列,我們可以使用我們的值列表填充陣列:

data['name'] = name
data['age'] = age
data['weight'] = weight
print(data)

'''
[('Alice', 25, 55.0) ('Bob', 45, 85.5) ('Cathy', 37, 68.0)
 ('Doug', 19, 61.5)]
'''

正如我們所希望的那樣,資料現在被安排在一個方便的記憶體塊中。結構化陣列的便利之處在於,你現在可以通過索引或名稱來引用值:

# 獲取所有名稱
data['name']

'''
array(['Alice', 'Bob', 'Cathy', 'Doug'], 
      dtype='<U10')
'''

# 獲取資料的第一行
data[0]

# ('Alice', 25, 55.0)

# 獲取最後一行的名稱
data[-1]['name']

# 'Doug'

使用布林掩碼,你甚至可以執行一些更復雜的操作,例如過濾年齡:

# 獲取年齡小於 30 的名稱
data[data['age'] < 30]['name']

'''
array(['Alice', 'Doug'], 
      dtype='<U10')
'''

請注意,如果你想進行任何比這些更復雜的操作,你應該考慮下一章中介紹的 Pandas 包。正如我們所看到的,Pandas 提供了Dataframe物件,它是一個構建在 NumPy 陣列上的結構,它提供了各種有用的資料操作功能,類似於我們在這裡展示的東西,以及更多。

建立結構化陣列

可以通過多種方式規定結構化陣列資料型別。之前,我們見過了字典方法:

np.dtype({'names':('name', 'age', 'weight'),
          'formats':('U10', 'i4', 'f8')})

# dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])

為清楚起見,可以使用 Python 型別或 NumPy dtype來指定數字型別:

np.dtype({'names':('name', 'age', 'weight'),
          'formats':((np.str_, 10), int, np.float32)})
          
# dtype([('name', '<U10'), ('age', '<i8'), ('weight', '<f4')])

複合型別也可以指定為元組列表:

np.dtype([('name', 'S10'), ('age', 'i4'), ('weight', 'f8')])

# dtype([('name', 'S10'), ('age', '<i4'), ('weight', '<f8')])

如果型別的名稱對你無關緊要,則可以在逗號分隔的字串中單獨指定型別:

np.dtype('S10,i4,f8')

# dtype([('f0', 'S10'), ('f1', '<i4'), ('f2', '<f8')])

縮短的字串格式程式碼可能看起來令人困惑,但它們建立在簡單的原則之上。第一個(可選)字元是<>,分別表示“小端”或“大端”,並規定了有效位的順序約定。下一個字元指定資料型別:字元,位元組,整數,浮點等(參見下表)。最後一個或多個字元表示物件的大小(以位元組為單位)。

字元 描述 示例
'b' 位元組 np.dtype('b')
'i' 符號整數 np.dtype('i4') == np.int32
'u' 無符號整數 np.dtype('u1') == np.uint8
'f' 浮點 np.dtype('f8') == np.int64
'c' 複數浮點 np.dtype('c16') == np.complex128
'S', 'a' 字串 np.dtype('S5')
'U' Unicode 字串 np.dtype('U') == np.str_
'V' 原始資料(void) np.dtype('V') == np.void

更高階的複合型別

可以定義更高階的複合型別。例如,你可以建立一個型別,其中每個元素包含一個數組或矩陣。在這裡,我們將建立一個帶有mat成分的資料型別,該成分由3x3浮點矩陣組成:

tp = np.dtype([('id', 'i8'), ('mat', 'f8', (3, 3))])
X = np.zeros(1, dtype=tp)
print(X[0])
print(X['mat'][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.]]
'''

現在X陣列中的每個元素都包含一個id和一個3x3矩陣。為什麼要使用它而不是簡單的多維陣列,或者 Python 字典呢?原因是這個 NumPy dtype直接對映到 C 結構定義,因此包含陣列內容的緩衝區,可以在適當編寫的 C 程式中直接訪問。

如果你發現自己為處理結構化資料的遺留 C 或 Fortran 庫編寫 Python 介面,你可能會發現結構化陣列非常有用!

記錄陣列:略有不同的結構化陣列

NumPy 還提供了np.recarray類,它與剛剛描述的結構化陣列幾乎相同,但有一個附加功能:欄位可以作為屬性而不是字典的鍵來訪問。

回想一下,我們以前寫過:

data['age']

# array([25, 45, 37, 19], dtype=int32)

如果我們將資料視為記錄陣列,我們可以通過更少的敲鍵盤來訪問它:

data_rec = data.view(np.recarray)
data_rec.age

# array([25, 45, 37, 19], dtype=int32)

缺點是對於記錄陣列,即使使用相同的語法,訪問欄位會有一些額外的開銷。 我們在這裡可以看到:

%timeit data['age']
%timeit data_rec['age']
%timeit data_rec.age

'''
1000000 loops, best of 3: 241 ns per loop
100000 loops, best of 3: 4.61 µs per loop
100000 loops, best of 3: 7.27 µs per loop
'''

更方便的記號是否值得額外開銷,取決於你自己的應用。

轉向 Pandas

關於結構化和記錄陣列的這一部分,有意放在本章的最後部分,因為它很好地介紹了我們將要介紹的下一個包:Pandas。

在某些情況下,最好了解這裡討論的結構化陣列,特別是在你使用 NumPy 陣列來對映到 C,Fortran 或其他語言的二進位制資料格式的情況下。

對於結構化資料的日常使用,Pandas 包是一個更好的選擇,我們將在下一章中深入討論它。