NumPy 提供了高效的陣列操作功能,對於資料分析和科學計算至關重要。理解這些功能的應用場景和效能差異,能幫助我們選擇最佳的解決方案。尤其在處理大量資料時,NumPy 的向量化操作和廣播機制能大幅提升運算效率。以下將會深入探討 NumPy 陣列的排序、分組以及結構化資料的處理技巧。

資料分組和統計

NumPy還提供了資料分組和統計的功能。例如,使用np.searchsorted函式可以找到每個資料點在陣列中的位置。然後,使用np.add.at函式可以實作分組統計。

示例:資料分組和統計

以下示例展示瞭如何使用NumPy進行資料分組和統計:

import numpy as np

# 生成隨機資料
rng = np.random.default_rng(seed=1701)
x = rng.normal(size=100)

# 定義分組邊界
bins = np.linspace(-5, 5, 20)

# 初始化分組計數
counts = np.zeros_like(bins)

# 找到每個資料點在陣列中的位置
i = np.searchsorted(bins, x)

# 對每個分組進行計數
np.add.at(counts, i, 1)

這段程式碼會計算出每個分組中的資料點數量,並儲存在counts陣列中。

Matplotlib中的直方圖函式

Matplotlib提供了plt.hist函式來繪製直方圖。這個函式會自動計算出每個分組中的資料點數量,並繪製出直方圖。

import matplotlib.pyplot as plt

# 繪製直方圖
plt.hist(x, bins, histtype='step');

這段程式碼會生成一個與上述示例中計算出的直方圖相同的圖形。

圖表翻譯:

以下是上述程式碼中使用的Plantuml圖表: 這個圖表展示了資料分組和統計的過程。

效能比較:NumPy 直方圖與自訂直方圖

在探討資料分析的效能時,瞭解不同演算法之間的效能差異至關重要。在這個例子中,我們比較了 NumPy 的 np.histogram 函式與自訂的直方圖實作。自訂實作使用 np.searchsortednp.add.at 函式來計算直方圖。

import numpy as np

# 產生隨機資料
x = np.random.normal(size=100)

# 定義直方圖的 bin 數量
bins = 10

# 使用 NumPy 的 np.histogram 函式
counts, edges = np.histogram(x, bins)

# 自訂直方圖實作
counts_custom = np.zeros(bins)
np.add.at(counts_custom, np.searchsorted(bins, x), 1)

使用 %timeit 魔法函式來比較兩個實作的效能:

%timeit counts, edges = np.histogram(x, bins)
%timeit np.add.at(counts_custom, np.searchsorted(bins, x), 1)

結果顯示,自訂實作的效能約為 NumPy 的 np.histogram 函式的兩倍。然而,當資料點數量增加到 1000000 時,NumPy 的 np.histogram 函式的效能優勢變得明顯。

x = np.random.normal(size=1000000)
%timeit counts, edges = np.histogram(x, bins)
%timeit np.add.at(counts_custom, np.searchsorted(bins, x), 1)

這個比較結果表明,演算法的效能取決於資料的大小和複雜度。對於小型資料集,自訂實作可能更快,但對於大型資料集,NumPy 的最佳化實作可能更有效。

內容解密:

  • np.histogram 函式:計算資料的直方圖。
  • np.searchsorted 函式:查詢資料在排序後陣列中的索引。
  • np.add.at 函式:在指定索引處累加值。
  • %timeit 魔法函式:計算執行時間。

圖表翻譯:

這個流程圖描述了資料分析的過程,從資料產生到直方圖計算、效能比較,最終到結果顯示。

排序演算法與NumPy

在之前的章節中,我們主要探討了使用NumPy存取和操作陣列資料的工具。在這個章節中,我們將深入探討與排序NumPy陣列中的值相關的演算法。排序演算法是電腦科學入門課程中的一個熱門話題,如果你曾經上過這類別課程,你可能曾經對插入排序、選擇排序、合併排序、快速排序、氣泡排序等有所耳聞。所有這些都是用於完成同一任務的不同方法:排序列表或陣列中的值。

Python有一些內建函式和方法可以用於排序列表和其他可迭代物件。sorted函式接受一個列表並傳回其排序版本:

L = [3, 1, 4, 1, 5, 9, 2, 6]
sorted(L)  # 傳回排序後的複製品

輸出:

[1, 1, 2, 3, 4, 5, 6, 9]

而列表的sort方法則會就地排序列表:

L.sort()  # 就地排序並傳回None
print(L)

輸出:

[1, 1, 2, 3, 4, 5, 6, 9]

Python的排序方法非常靈活,可以處理任何可迭代物件。例如,以下是對字串進行排序:

sorted('python')

輸出:

['h', 'n', 'o', 'p', 't', 'y']

這些內建的排序方法很方便,但如前所述,由於Python值的動態性,這些方法的效能不如為統一數字陣列設計的特定程式。這就是NumPy的排序程式的用途。

NumPy中的快速排序:np.sortnp.argsort

np.sort函式與Python的內建sorted函式類別似,會高效地傳回陣列的排序複製品:

import numpy as np

x = np.array([2, 1, 4, 3, 5])
np.sort(x)

輸出:

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

與Python列表的sort方法類別似,你也可以使用陣列的sort方法就地排序陣列:

x.sort()

內容解密:

上述程式碼展示瞭如何使用NumPy的np.sort函式和陣列的sort方法對陣列進行排序。這些方法可以高效地對統一數字陣列進行排序,優於Python的內建排序方法。以下是程式碼的逐步解釋:

  1. 匯入NumPy函式庫並建立一個示例陣列x
  2. 使用np.sort函式傳回陣列x的排序複製品。
  3. 使用陣列的sort方法就地排序陣列x

這些方法可以用於對NumPy陣列進行快速和高效的排序,尤其是在處理大型資料集時。

排序與索引:NumPy陣列的強大工具

在處理資料時,排序和索引是兩個非常重要的功能。NumPy提供了多種方法來對陣列進行排序和索引,讓我們可以更有效地管理和分析資料。

排序陣列

NumPy的sort函式可以用來對陣列進行排序。以下是如何使用它的例子:

import numpy as np

x = np.array([2, 1, 4, 3, 5])
x_sorted = np.sort(x)
print(x_sorted)

輸出結果將是 [1 2 3 4 5],即排序後的陣列。

取得排序後的索引

如果我們想要獲得排序後的索引,而不是排序後的陣列本身,可以使用argsort函式。以下是如何使用它的例子:

import numpy as np

x = np.array([2, 1, 4, 3, 5])
i = np.argsort(x)
print(i)

輸出結果將是 [1 0 3 2 4],即排序後的索引。

使用索引進行排序

如果我們想要使用索引進行排序,可以使用以下方法:

import numpy as np

x = np.array([2, 1, 4, 3, 5])
i = np.argsort(x)
x_sorted = x[i]
print(x_sorted)

輸出結果將是 [1 2 3 4 5],即排序後的陣列。

排序多維陣列

NumPy的排序演算法也可以用來對多維陣列進行排序。以下是如何使用axis引數來指定排序軸的例子:

import numpy as np

rng = np.random.default_rng(seed=42)
X = rng.integers(0, 10, (4, 6))
print(X)

# 對每行進行排序
X_sorted_row = np.sort(X, axis=1)
print(X_sorted_row)

# 對每列進行排序
X_sorted_col = np.sort(X, axis=0)
print(X_sorted_col)

這些功能讓我們可以更有效地管理和分析資料,特別是在處理大型資料集時。

排序和分割:NumPy 的強大工具

在資料分析和科學計算中,排序和分割是兩個非常重要的操作。NumPy 提供了多種方法來實作這些操作,包括 np.sortnp.partitionnp.argsortnp.argpartition

排序

np.sort 函式可以用來排序 NumPy 陣列中的元素。它可以根據指定的軸(axis)進行排序,例如,沿著行(axis=1)或列(axis=0)進行排序。

import numpy as np

# 定義一個 2D 陣列
X = np.array([[8, 4, 5, 3, 1, 9],
              [0, 6, 2, 0, 4, 8],
              [7, 7, 6, 4, 5, 9],
              [1, 3, 4, 5, 8, 9]])

# 對每一列進行排序
sorted_X_axis0 = np.sort(X, axis=0)
print(sorted_X_axis0)

# 對每一行進行排序
sorted_X_axis1 = np.sort(X, axis=1)
print(sorted_X_axis1)

分割

np.partition 函式可以用來分割 NumPy 陣列中的元素。它可以根據指定的 k 值,將陣列分割成兩部分:前 k 個最小的元素和剩餘的元素。

# 定義一個 1D 陣列
x = np.array([7, 2, 3, 1, 6, 5, 4])

# 對陣列進行分割,取出前 3 個最小的元素
partitioned_x = np.partition(x, 3)
print(partitioned_x)

# 對 2D 陣列進行分割,沿著行軸(axis=1)
partitioned_X_axis1 = np.partition(X, 2, axis=1)
print(partitioned_X_axis1)

排序和分割的索引

np.argsortnp.argpartition 函式可以用來計算排序和分割後的索引。

# 定義一個 1D 陣列
x = np.array([7, 2, 3, 1, 6, 5, 4])

# 計算排序後的索引
argsorted_x = np.argsort(x)
print(argsorted_x)

# 計算分割後的索引
argpartitioned_x = np.argpartition(x, 3)
print(argpartitioned_x)

這些函式可以幫助您高效地進行資料分析和科學計算。

使用 argsort 函式尋找最近鄰居

在這個例子中,我們將使用 argsort 函式沿著多個軸尋找一組點的最近鄰居。首先,我們生成 10 個隨機點,並將它們安排在一個 10x2 的陣列中。

import numpy as np

# 生成 10 個隨機點
X = np.random.rand(10, 2)

接下來,我們計算每對點之間的距離。距離的平方是每個維度中差異的平方和。利用 NumPy 的廣播和聚合功能,我們可以高效地計算這些距離。

# 計算每對點之間的距離
dist_sq = np.sum((X[:, np.newaxis] - X[np.newaxis, :]) ** 2, axis=-1)

這個操作包含了很多步驟,如果你不熟悉 NumPy 的廣播規則,可能會有些混淆。為了更好地理解這個過程,我們可以將它分解成幾個步驟:

# 為每對點計算坐標差異
differences = X[:, np.newaxis] - X[np.newaxis, :]

# 對坐標差異求平方
sq_differences = differences ** 2

# 對坐標差異求和,得到距離的平方
dist_sq = sq_differences.sum(-1)

為了驗證我們的邏輯,讓我們檢查這個矩陣的對角線(即每個點與自身的距離),應該全部為零。

# 檢查對角線元素是否全部為零
print(dist_sq.diagonal())

最後,利用 np.argsort 函式沿著每行排序,我們可以得到最近鄰居的索引。

# 對每行排序,得到最近鄰居的索引
nearest_neighbors = np.argsort(dist_sq, axis=1)

內容解密:

在這個例子中,我們使用 argsort 函式沿著多個軸尋找最近鄰居。首先,我們生成了一組隨機點,並計算每對點之間的距離。然後,我們利用 NumPy 的廣播和聚合功能高效地計算這些距離。最後,利用 np.argsort 函式沿著每行排序,我們得到最近鄰居的索引。

圖表翻譯:

這個流程圖展示了我們如何使用 argsort 函式尋找最近鄰居。首先,我們生成一組隨機點,然後計算每對點之間的距離。接下來,我們利用 NumPy 的廣播和聚合功能高效地計算這些距離。最後,利用 np.argsort 函式沿著每行排序,我們得到最近鄰居的索引。

最近鄰居搜尋

在進行最近鄰居搜尋時,我們的目標是找出給定資料點集合中,每個點的最近鄰居。這可以透過計算每個點與其他所有點之間的距離來實作。

距離計算

距離計算是最近鄰居搜尋的基礎。給定兩個點 $x_i$ 和 $x_j$,我們可以使用歐幾裡得距離公式計算它們之間的距離:

$$d_{ij} = \sqrt{(x_i - x_j)^2}$$

在 Python 中,我們可以使用 NumPy 來高效地計算距離。假設我們有 $n$ 個資料點,每個點有 $m$ 個特徵,則距離矩陣 $D$ 的尺寸為 $n \times n$。

import numpy as np

# 假設 X 是一個 (n, m) 的資料矩陣
X = np.random.rand(10, 2)  # 10 個資料點,每個點有 2 個特徵

# 計算距離矩陣
dist_sq = np.sum((X[:, None, :] - X[None, :, :]) ** 2, axis=-1)

最近鄰居搜尋

計算距離矩陣後,我們可以使用 np.argsort 函式來找到每個點的最近鄰居。

# 找到每個點的最近鄰居
nearest = np.argsort(dist_sq, axis=1)

由於每個點與自身的距離為 0,因此第一列將包含每個點自身的索引。

最近 k 個鄰居

如果我們只對每個點的最近 k 個鄰居感興趣,我們可以使用 np.argpartition 函式來加速計算。

K = 2
nearest_partition = np.argpartition(dist_sq, K + 1, axis=1)

這將使得每行中最小的 k + 1 個距離值排在前面,其餘距離值則填充在剩餘位置。

視覺化

最後,我們可以使用 Matplotlib 來視覺化資料點及其之間的連線。

import matplotlib.pyplot as plt

plt.scatter(X[:, 0], X[:, 1], s=100)

# 畫出每個點到其最近 k 個鄰居的連線線
for i in range(len(X)):
    for j in nearest_partition[i, :K + 1]:
        plt.plot([X[i, 0], X[j, 0]], [X[i, 1], X[j, 1]], 'k-')

這將產生一幅圖,顯示資料點及其之間的連線,從而直觀地展示最近鄰居搜尋的結果。

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

在處理資料時,我們常常遇到需要儲存不同型別的資料,例如人名、年齡和體重等。這種情況下,使用NumPy的結構化陣列(Structured Arrays)可以提供一個更自然和高效的方式來儲存和操作這些資料。

基本概念

NumPy的結構化陣列允許您定義一個包含多個欄位的陣列,每個欄位可以有不同的資料型別。這使得您可以將相關的資料儲存在一起,並使用一個單一的陣列來表示。

建立結構化陣列

要建立一個結構化陣列,您需要定義一個複合資料型別(Compound Data Type)。這可以透過使用dtype引數來完成。例如:

import numpy as np

# 定義一個複合資料型別
dtype = {'names': ('name', 'age', 'weight'), 
         'formats': ('U10', 'i4', 'f8')}

# 建立一個結構化陣列
data = np.zeros(4, dtype=dtype)

在這個例子中,我們定義了一個包含三個欄位的複合資料型別:nameageweight。每個欄位都有不同的資料型別:name是字串(U10),age是整數(i4),而weight是浮點數(f8)。

存取結構化陣列的欄位

一旦您建立了結構化陣列,您就可以存取其欄位使用點符號(.)。例如:

print(data['name'])  # 輸出:['' '' '' '']
print(data['age'])   # 輸出:[0 0 0 0]
print(data['weight'])  # 輸出:[0. 0. 0. 0.]

您也可以使用索引來存取結構化陣列的元素。例如:

print(data[0]['name'])  # 輸出:''
print(data[1]['age'])   # 輸出:0
print(data[2]['weight'])  # 輸出:0.0

結構化陣列的優點

使用結構化陣列有幾個優點:

  • 更自然的資料表示:結構化陣列允許您將相關的資料儲存在一起,這使得您的程式碼更容易閱讀和維護。
  • 更高效的資料儲存:結構化陣列可以比傳統的陣列更高效地儲存資料,因為它們只需要儲存每個欄位的資料型別。
  • 更強大的資料操作:結構化陣列提供了一系列的方法來操作和分析資料,例如排序、過濾和分組。

結構化陣列的建立與操作

在上一節中,我們瞭解瞭如何使用NumPy建立結構化陣列。現在,我們將更深入地探討結構化陣列的操作。

首先,讓我們建立一個空的結構化陣列容器:

import numpy as np

# 定義欄位名稱和資料型態
dtype = [('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]

# 建立空的結構化陣列
data = np.empty(4, dtype=dtype)

接下來,我們可以填充陣列中的值:

# 定義名稱、年齡和體重的列表
name = ['Alice', 'Bob', 'Cathy', 'Doug']
age = [25, 45, 37, 19]
weight = [55., 85.5, 68., 61.5]

# 將值填充到陣列中
data['name'] = name
data['age'] = age
data['weight'] = weight

現在,讓我們印出陣列的內容:

print(data)

輸出結果:

array([('Alice', 25, 55.), ('Bob', 45, 85.5), ('Cathy', 37, 68.),
       ('Doug', 19, 61.5)],
      dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])

如我們所見,資料現在被整齊地安排在一個結構化陣列中。

結構化陣列的優點之一是,我們可以根據欄位名稱存取值。例如:

# 取得所有名稱
print(data['name'])

輸出結果:

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

或者,我們可以根據索引存取值:

# 取得第一行資料
print(data[0])

輸出結果:

('Alice', 25, 55.)

甚至,我們可以使用布林遮罩(Boolean masking)進行更複雜的操作,例如篩選年齡:

# 篩選年齡大於30的人
mask = data['age'] > 30
print(data[mask])

輸出結果:

array([('Bob', 45, 85.5), ('Cathy', 37, 68.)],
      dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])

內容解密:

在這個例子中,我們使用NumPy建立了一個結構化陣列,並填充了名稱、年齡和體重的值。然後,我們展示瞭如何根據欄位名稱和索引存取值,並使用布林遮罩進行篩選。

圖表翻譯:

下面是使用Plantuml語法繪製的流程圖,展示了結構化陣列的建立和操作過程: 這個流程圖展示了結構化陣列的建立、填充、存取和篩選的過程。

結構化陣列的建立和操作

在 NumPy 中,結構化陣列(Structured Array)是一種複合資料型態,可以用來儲存不同欄位的資料。以下是建立結構化陣列的幾種方法:

1. 使用字典方法

可以使用字典來定義結構化陣列的欄位和資料型態。例如:

import numpy as np

# 定義結構化陣列的欄位和資料型態
dtype = np.dtype({'names': ('name', 'age', 'weight'),
                  'formats': ('U10', 'i4', 'f8')})

print(dtype)

輸出:

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

2. 使用元組列表方法

也可以使用元組列表來定義結構化陣列的欄位和資料型態。例如:

import numpy as np

# 定義結構化陣列的欄位和資料型態
dtype = np.dtype([('name', 'S10'), ('age', 'i4'), ('weight', 'f8')])

print(dtype)

輸出:

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

3. 使用字串方法

如果不需要指定欄位名稱,可以使用字串來定義結構化陣列的資料型態。例如:

import numpy as np

# 定義結構化陣列的資料型態
dtype = np.dtype('S10,i4,f8')

print(dtype)

輸出:

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

資料型態程式碼

NumPy 的資料型態程式碼是由三部分組成:

  • 第一部分(可選): <>,表示 little-endian 或 big-endian。
  • 第二部分:資料型態程式碼,例如 biuf 等。
  • 第三部分:資料大小,例如 148 等。

以下是 NumPy 的資料型態程式碼表:

程式碼說明例子
bBytenp.dtype('b')
iSigned integernp.dtype('i4') == np.int32
uUnsigned integernp.dtype('u1') == np.uint8
fFloating pointnp.dtype('f8') == np.float64

內容解密:

在上面的例子中,我們使用了不同的方法來定義結構化陣列的欄位和資料型態。使用字典方法可以指定欄位名稱和資料型態,而使用元組列表方法可以指定欄位名稱和資料型態。使用字串方法可以簡單地指定資料型態。

圖表翻譯:

以下是使用 Plantuml 圖表來展示結構化陣列的建立和操作:

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title NumPy 陣列排序分組與結構化資料操作技巧

package "資料視覺化流程" {
    package "資料準備" {
        component [資料載入] as load
        component [資料清洗] as clean
        component [資料轉換] as transform
    }

    package "圖表類型" {
        component [折線圖 Line] as line
        component [長條圖 Bar] as bar
        component [散佈圖 Scatter] as scatter
        component [熱力圖 Heatmap] as heatmap
    }

    package "美化輸出" {
        component [樣式設定] as style
        component [標籤註解] as label
        component [匯出儲存] as export
    }
}

load --> clean --> transform
transform --> line
transform --> bar
transform --> scatter
transform --> heatmap
line --> style --> export
bar --> label --> export

note right of scatter
  探索變數關係
  發現異常值
end note

@enduml

在這個圖表中,我們展示了三種不同的方法來定義結構化陣列,並且每種方法都可以指定欄位名稱和資料型態。

從資料處理效能的角度來看,NumPy 提供了強大的工具,例如 np.histogramnp.sortnp.partitionnp.argsortnp.argpartition 等函式,可以高效地進行直方圖計算、排序、分割和搜尋最近鄰居等操作。尤其是在處理大型資料集時,NumPy 的向量化操作和最佳化演算法能夠顯著提升效能,相較於自訂的 Python 迴圈方案,效率提升尤為明顯。

然而,NumPy 的效能優勢並非絕對。對於小型資料集,自訂的 Python 迴圈方案有時反而更快。例如,在計算直方圖時,對於少量資料點,使用 np.searchsortednp.add.at 的自訂方案可能比 np.histogram 更快。因此,在實際應用中,需要根據資料集的大小和操作的複雜度來選擇合適的方案,才能達到最佳的效能。

此外,NumPy 的結構化陣列提供了一種更自然和高效的方式來儲存和操作不同型別的資料。透過定義複合資料型態,可以將相關的資料儲存在一起,並使用點符號或索引來存取欄位,簡化了資料處理的流程。同時,結構化陣列也支援布林遮罩等進階操作,方便進行資料篩選和分析。

展望未來,隨著資料量的持續增長和演算法的日益複雜,高效的資料處理工具將變得更加重要。NumPy 作為 Python 科學計算的核心函式庫,將持續扮演重要的角色。預計未來 NumPy 將會在效能最佳化、功能擴充套件和與其他資料科學工具的整合方面持續發展,為資料科學家和工程師提供更強大的工具。對於臺灣的開發者而言,深入理解和掌握 NumPy 的使用技巧,將有助於提升資料處理能力,並在資料驅動的時代保持競爭力。玄貓認為,NumPy 雖然功能強大,但也需要謹慎使用,避免不必要的資料複製和複雜操作,才能最大限度地發揮其效能優勢。