脱Pythonループ!NumPy配列処理のベクトル化テクニック

2025-05-27

NumPyの配列(ndarray)は、Pythonの標準的なリストなどと同様に、要素を一つずつ取り出して処理する「イテレーション」を行うことができます。しかし、NumPyの配列は多次元であるため、イテレーションの動作にはいくつかの特徴と、効率的な方法が存在します。

基本的なイテレーション (一次元配列の場合)

一次元配列の場合、Pythonのforループを使って直接イテレーションすることができます。これは通常のPythonリストと同じ感覚です。

import numpy as np

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

print("一次元配列のイテレーション:")
for x in arr:
    print(x)

出力:

一次元配列のイテレーション:
1
2
3
4
5

多次元配列のイテレーション

多次元配列の場合、forループで直接イテレーションすると、最も外側の軸(次元)に沿って要素(スライス)が返されます。 これは、配列の「行」に相当する部分を取り出すようなイメージです。

import numpy as np

arr_2d = np.array([[1, 2, 3], [4, 5, 6]])

print("\n二次元配列のイテレーション (行ごと):")
for row in arr_2d:
    print(row)
二次元配列のイテレーション (行ごと):
[1 2 3]
[4 5 6]

これは、二次元配列の場合は各行が、三次元配列の場合は各「層」が返されることを意味します。

全ての要素をイテレーションする (flat 属性)

多次元配列の全ての要素を個別にイテレーションしたい場合は、ndarrayオブジェクトのflat属性を使用するのが最も簡単で推奨される方法です。flatはイテレータを返すため、メモリ効率も良いです。

import numpy as np

arr_2d = np.array([[1, 2, 3], [4, 5, 6]])

print("\n全ての要素をイテレーション (flat):")
for element in arr_2d.flat:
    print(element)
全ての要素をイテレーション (flat):
1
2
3
4
5
6

flat属性を使うと、配列の要素が1つずつ、平坦化された順序(C-contiguous order、つまり行優先)で返されます。

nditer オブジェクト (高度なイテレーション)

NumPyは、より高度で柔軟なイテレーションを可能にするnp.nditerオブジェクトを提供しています。これは、要素の順序を制御したり、複数の配列を同時にイテレーションしたり、特定のデータ型でイテレーションしたりする場合に非常に強力です。

基本的な使い方はflatと似ていますが、より多くのオプションがあります。

import numpy as np

arr_2d = np.array([[1, 2, 3], [4, 5, 6]])

print("\nnditer を使用したイテレーション:")
for x in np.nditer(arr_2d):
    print(x)
nditer を使用したイテレーション:
1
2
3
4
5
6

nditerの主な機能としては以下のようなものがあります。

  • 外部ループの制御
    より大きなチャンクでデータを処理し、パフォーマンスを向上させることができます。
  • ブロードキャストされたイテレーション
    異なる形状の配列を同時にイテレーションし、ブロードキャスト規則に従って要素を組み合わせることができます。
  • 読み書きモード (op_flags)
    イテレーション中に要素の値を変更したい場合に使用します。
    arr_writable = np.array([[1, 2], [3, 4]])
    print("\nイテレーション中に要素を書き換え:")
    for x in np.nditer(arr_writable, op_flags=['readwrite']):
        x[...] = x * 2 # 要素を2倍にする
    print(arr_writable)
    
    出力:
    イテレーション中に要素を書き換え:
    [[2 4]
     [6 8]]
    
  • 順序の制御 (order引数)
    例えば、Fortran順(列優先)でイテレーションすることも可能です。
    print("\nnditer で Fortran 順にイテレーション:")
    for x in np.nditer(arr_2d, order='F'):
        print(x)
    
    出力:
    nditer で Fortran 順にイテレーション:
    1
    4
    2
    5
    3
    6
    

np.ndenumerate (要素とインデックスのイテレーション)

要素とそのインデックスを同時に取得したい場合は、np.ndenumerateを使用します。これは、enumerateがリストで行うように、NumPy配列に対して機能します。

import numpy as np

arr_2d = np.array([[10, 20], [30, 40]])

print("\nndenumerate を使用したイテレーション (インデックスと値):")
for index, value in np.ndenumerate(arr_2d):
    print(f"Index: {index}, Value: {value}")
ndenumerate を使用したイテレーション (インデックスと値):
Index: (0, 0), Value: 10
Index: (0, 1), Value: 20
Index: (1, 0), Value: 30
Index: (1, 1), Value: 40

indexは、要素の次元数に応じたタプルで返されます。

  • インデックスと値を同時に取得
    np.ndenumerate を使用。
  • 高度な制御が必要な場合
    np.nditer を使用。
  • 全ての要素を平坦にイテレーション
    arr.flat を使うのが最も一般的で簡単。
  • 多次元配列
    for x in arr: だと外側の軸(行)が返される。
  • 一次元配列
    for x in arr: で直接イテレーション。


NumPy配列のイテレーションにおける共通エラーとトラブルシューティング

NumPy配列のイテレーションは、Pythonの通常のforループと似ている部分もありますが、多次元性やNumPy特有の最適化メカニズムのために、予期せぬ挙動やエラーが発生することがあります。

想定外の次元でのイテレーション

問題
多次元配列を通常のforループでイテレーションすると、期待する「要素ごと」ではなく、「最も外側の軸(次元)に沿って」イテレーションされます。例えば、2次元配列では行ごとにイテレーションされます。

import numpy as np

arr_2d = np.array([[1, 2, 3], [4, 5, 6]])

print("通常のイテレーション:")
for item in arr_2d:
    print(item)
# 期待: 1, 2, 3, 4, 5, 6
# 実際: [1 2 3], [4 5 6]

トラブルシューティング

  • 特定の軸に沿ってイテレーションしたい場合
    NumPyの機能を使うか、明示的にインデックス指定します。例えば、列ごとに処理したい場合は転置(arr.T)してからイテレーションする方法もあります。
    print("\n転置して列ごとにイテレーション:")
    for column in arr_2d.T:
        print(column)
    
  • 全ての要素を個別に処理したい場合
    arr.flat属性を使用します。
    print("\nflat を使用したイテレーション:")
    for element in arr_2d.flat:
        print(element)
    

要素の型変換エラー (TypeError: 'numpy.ndarray' object is not callable など)

問題
NumPy配列の要素をイテレーションして、Pythonの組み込み関数(例: int(), str())や他のNumPy関数に渡す際に、要素の型が期待通りでない、あるいは配列全体が渡されてしまうことがあります。特に、for element in arr:elementがNumPyのスカラーではなく、サブ配列である場合に発生しやすいです。

import numpy as np

arr = np.array([1.1, 2.2, 3.3])

# 問題: 要素の型を整数に変換したいが、Pythonのint()はNumPyのスカラーに直接適用される
# arr_int = [int(x) for x in arr] # これは動作する

# しかし、多次元配列の場合、ループ内の要素がスカラーでないとエラーになることがある
arr_2d = np.array([[1.1, 2.2], [3.3, 4.4]])
# for row in arr_2d:
#     print(int(row)) # TypeError: only size-1 arrays can be converted to Python scalars

トラブルシューティング

  • 要素がndarrayオブジェクトの場合の誤操作
    arr[0]ndarrayであるにもかかわらず、関数のように呼び出そうとするとTypeError: 'numpy.ndarray' object is not callableといったエラーが発生します。これはイテレーションとは直接関係ないこともありますが、イテレーション中に取り出した要素に対して誤った操作をした際に起こりえます。
  • NumPyの型変換関数を使用する
    arr.astype(int)のように、配列全体を型変換するNumPyの機能がより効率的です。
    arr_int = arr_2d.astype(int)
    print("\nastype を使って配列全体を型変換:")
    print(arr_int)
    
  • flat属性を使って個々のスカラー要素を取得する
    arr_2d = np.array([[1.1, 2.2], [3.3, 4.4]])
    print("\nflat を使って要素型変換:")
    for element in arr_2d.flat:
        print(int(element))
    

パフォーマンスの問題 (Pythonループの遅さ)

問題
NumPyはC言語などで最適化されているため、PythonのforループでNumPy配列の各要素を1つずつ処理すると、非常に遅くなることがあります。これは、NumPyの主な利点である高速なベクトル化演算を活かせていないためです。

import numpy as np
import time

size = 10**7
arr = np.arange(size)

start_time = time.time()
# 遅い処理の例: Pythonのforループで各要素に1を加算
# result_list = []
# for x in arr:
#     result_list.append(x + 1)
# print(f"Python for loop time: {time.time() - start_time:.4f} seconds")


start_time = time.time()
# 期待される速い処理: NumPyのベクトル化演算
result_numpy = arr + 1
print(f"NumPy vectorized operation time: {time.time() - start_time:.4f} seconds")

トラブルシューティング

  • np.nditerやnp.vectorizeを検討する(限定的)
    • np.nditer: 特定の複雑なイテレーションパターンで、かつPythonループのオーバーヘッドを減らしたい場合に有用です。ただし、それでもベクトル化演算には劣ります。
    • np.vectorize: Pythonの関数をNumPyのユニバーサル関数のように振る舞わせるためのものです。内部でPythonループが走るため、パフォーマンス向上は限定的ですが、コードの可読性を高めるのに役立つ場合があります。
  • 可能な限りベクトル化演算を使用する
    ほとんどの場合、NumPyの組み込み関数(ユニバーサル関数 ufunc)や演算子(+, -, *, /など)は、内部的に最適化されたCコードで動作するため、Pythonのループよりも圧倒的に高速です。
    • 要素ごとの算術演算: arr + 1, arr * 2, np.sqrt(arr)
    • 条件に基づく選択: arr[arr > 5], np.where(condition, x, y)
    • 集計: arr.sum(), arr.mean(), arr.max()

要素の書き換えが反映されない (nditerのop_flagsを忘れる)

問題
np.nditerを使って配列の要素をイテレーション中に変更しようとしても、デフォルトでは変更が元の配列に反映されません。

import numpy as np

arr = np.array([1, 2, 3])

print("変更前:", arr)
for x in np.nditer(arr):
    x[...] = x * 2 # これでは元の配列は変更されない
print("変更後 (誤):", arr)

トラブルシューティング

  • np.nditerを使用する場合、要素を書き換えたい場合はop_flags=['readwrite']を指定する必要があります。
    arr = np.array([1, 2, 3])
    
    print("\n変更前:", arr)
    for x in np.nditer(arr, op_flags=['readwrite']):
        x[...] = x * 2 # これで元の配列が変更される
    print("変更後 (正):", arr)
    

インデックスと値の同時取得の混乱

問題
Pythonのリストではenumerate()を使ってインデックスと値を同時に取得するのが一般的ですが、NumPy配列に直接適用すると、多次元配列の場合にインデックスの形式が期待と異なることがあります。

import numpy as np

arr_2d = np.array([[10, 20], [30, 40]])

print("enumerate(arr_2d) の問題:")
for idx, row in enumerate(arr_2d):
    print(f"Index: {idx}, Row: {row}")
# 期待: (0,0)の値, (0,1)の値, (1,0)の値, (1,1)の値
# 実際: 0番目の行, 1番目の行
  • np.ndenumerateを使用する
    全ての要素とその多次元インデックスを同時に取得したい場合は、np.ndenumerateが最適です。
    print("\nnp.ndenumerate を使用:")
    for index, value in np.ndenumerate(arr_2d):
        print(f"Index: {index}, Value: {value}")
    
  • ベクトル化を第一に考える
    繰り返しになりますが、NumPyで最高のパフォーマンスを引き出すためには、Pythonのループを避け、NumPyが提供するベクトル化された操作やユニバーサル関数を最大限に活用することを常に意識することが重要です。イテレーションは最後の手段と考えるべきです。
  • 公式ドキュメントを参照する
    NumPyの公式ドキュメントは非常に詳細で、イテレーションに関する豊富な情報と例が掲載されています。
  • 小型の配列でテスト
    大規模な配列で問題が発生した場合、より小さな配列で同じコードを試してみて、挙動を把握します。
  • print()デバッグ
    ループの各ステップで、現在処理している要素の型や値、配列の形状などをprint()で出力して確認します。
  • エラーメッセージをよく読む
    NumPyのエラーメッセージは、問題の原因を特定するのに役立つ情報を含んでいます。特にTypeError, ValueError, IndexErrorなどを確認しましょう。


NumPy配列のイテレーションは、Pythonの標準的なforループを使う方法から、NumPy独自の最適化された方法までいくつかあります。

一次元配列の基本的なイテレーション

最もシンプルなケースです。Pythonのリストと同じように、要素が順に返されます。

import numpy as np

# 一次元配列の作成
arr_1d = np.array([10, 20, 30, 40, 50])

print("--- 1. 一次元配列のイテレーション ---")
for x in arr_1d:
    print(x)
--- 1. 一次元配列のイテレーション ---
10
20
30
40
50

多次元配列の基本的なイテレーション(外側の軸に沿って)

多次元配列を通常のforループでイテレーションすると、最も外側の軸(次元)に沿って「スライス」が返されます。2次元配列の場合は行が、3次元配列の場合は2次元の「層」が返されます。

import numpy as np

# 二次元配列の作成
arr_2d = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

print("\n--- 2. 二次元配列のイテレーション (行ごと) ---")
for row in arr_2d:
    print(row)

# 三次元配列の作成
arr_3d = np.array([[[1, 2], [3, 4]],
                   [[5, 6], [7, 8]]])

print("\n--- 2. 三次元配列のイテレーション (層ごと) ---")
for layer in arr_3d:
    print(layer)
--- 2. 二次元配列のイテレーション (行ごと) ---
[1 2 3]
[4 5 6]
[7 8 9]

--- 2. 三次元配列のイテレーション (層ごと) ---
[[1 2]
 [3 4]]
[[5 6]
 [7 8]]

全ての要素を平坦にイテレーション (.flat 属性)

多次元配列の全ての要素を1つずつ、平坦化された順序(C-contiguous order、行優先)で処理したい場合に最も便利で推奨される方法です。

import numpy as np

arr_2d = np.array([[1, 2, 3],
                   [4, 5, 6]])

print("\n--- 3. 全ての要素をイテレーション (.flat) ---")
for element in arr_2d.flat:
    print(element)

# 応用例: 全ての要素を2倍にする(新しい配列を作成)
doubled_elements = np.array([x * 2 for x in arr_2d.flat]).reshape(arr_2d.shape)
print("\n--- 3. flat とリスト内包表記で要素を2倍 (新しい配列) ---")
print(doubled_elements)

# 注意: 要素の変更は推奨されない(NumPyのベクトル化演算を使うべき)
# arr_2d_copy = arr_2d.copy()
# for element in arr_2d_copy.flat:
#     element *= 2 # この変更は元の配列には反映されない(Pythonの数値はイミュータブルなため)
# print(arr_2d_copy) # 変わっていない
--- 3. 全ての要素をイテレーション (.flat) ---
1
2
3
4
5
6

--- 3. flat とリスト内包表記で要素を2倍 (新しい配列) ---
[[ 2  4  6]
 [ 8 10 12]]

要素とインデックスを同時にイテレーション (np.ndenumerate)

Pythonのenumerateに相当する機能で、各要素とその多次元インデックス(タプル)を同時に取得できます。

import numpy as np

arr_2d = np.array([[10, 20, 30],
                   [40, 50, 60]])

print("\n--- 4. 要素とインデックスを同時にイテレーション (np.ndenumerate) ---")
for index, value in np.ndenumerate(arr_2d):
    print(f"Index: {index}, Value: {value}")

# 3次元配列の場合
arr_3d = np.array([[[1, 2], [3, 4]],
                   [[5, 6], [7, 8]]])

print("\n--- 4. 3次元配列での ndenumerate ---")
for index, value in np.ndenumerate(arr_3d):
    print(f"Index: {index}, Value: {value}")
--- 4. 要素とインデックスを同時にイテレーション (np.ndenumerate) ---
Index: (0, 0), Value: 10
Index: (0, 1), Value: 20
Index: (0, 2), Value: 30
Index: (1, 0), Value: 40
Index: (1, 1), Value: 50
Index: (1, 2), Value: 60

--- 4. 3次元配列での ndenumerate ---
Index: (0, 0, 0), Value: 1
Index: (0, 0, 1), Value: 2
Index: (0, 1, 0), Value: 3
Index: (0, 1, 1), Value: 4
Index: (1, 0, 0), Value: 5
Index: (1, 0, 1), Value: 6
Index: (1, 1, 0), Value: 7
Index: (1, 1, 1), Value: 8

高度なイテレーション (np.nditer)

np.nditerはNumPy配列のイテレーションにおいて最も強力で柔軟なツールです。順序の制御、読み書きモード、複数の配列の同時イテレーションなどが可能です。

基本的な要素ごとのイテレーション

flatと同様に全ての要素をイテレーションします。

import numpy as np

arr = np.array([[1, 2], [3, 4]])

print("\n--- 5.1. nditer を使った基本的なイテレーション ---")
for x in np.nditer(arr):
    print(x)
--- 5.1. nditer を使った基本的なイテレーション ---
1
2
3
4
イテレーション中に値を変更 (op_flags=['readwrite'])

op_flags=['readwrite']を指定することで、イテレーション中に要素の値を直接変更できます。

import numpy as np

arr_mutable = np.array([[1, 2], [3, 4]], dtype=np.int32) # dtypeを指定

print("\n--- 5.2. nditer で要素を書き換え (op_flags=['readwrite']) ---")
print("変更前:", arr_mutable)

with np.nditer(arr_mutable, op_flags=['readwrite']) as it:
    for x in it:
        x[...] = x * 10 # x[...] を使うことでビューとして値を変更

print("変更後:", arr_mutable)
--- 5.2. nditer で要素を書き換え (op_flags=['readwrite']) ---
変更前: [[1 2]
 [3 4]]
変更後: [[10 20]
 [30 40]]
イテレーション順序の制御 (order='F' など)

メモリ上の並び順に関わらず、特定の順序でイテレーションしたい場合にorder引数を使用します。

  • 'A' (Any order): 必要に応じてCまたはFortran順に選択
  • 'K' (Keep order): 現在のメモリレイアウトを維持 (デフォルト)
  • 'F' (Fortran-order / column-major): 列優先
  • 'C' (C-order / row-major): 行優先
import numpy as np

arr = np.array([[1, 2, 3],
                [4, 5, 6]])

print("\n--- 5.3. nditer で C-order (行優先) ---")
for x in np.nditer(arr, order='C'):
    print(x, end=' ')
print()

print("\n--- 5.3. nditer で F-order (列優先) ---")
for x in np.nditer(arr, order='F'):
    print(x, end=' ')
print()
--- 5.3. nditer で C-order (行優先) ---
1 2 3 4 5 6 

--- 5.3. nditer で F-order (列優先) ---
1 4 2 5 3 6 
複数の配列を同時にイテレーション (ブロードキャスト)

複数のNumPy配列を同時にイテレーションし、必要に応じてブロードキャスト規則に従って要素を組み合わせることができます。

import numpy as np

arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([10, 20]) # arr1の各行にブロードキャストされる

print("\n--- 5.4. nditer で複数の配列を同時にイテレーション ---")
for x, y in np.nditer([arr1, arr2]):
    print(f"{x} + {y} = {x + y}")
--- 5.4. nditer で複数の配列を同時にイテレーション ---
1 + 10 = 11
2 + 20 = 22
3 + 10 = 13
4 + 20 = 24

NumPyのベクトル化演算(イテレーションの代替として最も推奨)

NumPyの最大の利点は、要素ごとにPythonループを回す代わりに、配列全体に対して操作を行う「ベクトル化」された演算が非常に高速である点です。可能な限り、イテレーションの代わりにこれらのNumPy関数や演算子を使用すべきです。

import numpy as np
import time

size = 10**6
arr = np.arange(size)

print("\n--- 6. NumPyのベクトル化演算の例 (パフォーマンス比較) ---")

start_time = time.time()
# Pythonのループで加算(非常に遅い)
# result_python_loop = [x + 5 for x in arr]
# print(f"Python loop time: {time.time() - start_time:.6f} seconds")

start_time = time.time()
# NumPyのベクトル化演算で加算(非常に高速)
result_numpy_vectorized = arr + 5
print(f"NumPy vectorized time: {time.time() - start_time:.6f} seconds")

# 条件に基づく要素の選択
data = np.array([10, 5, 20, 15, 30])
filtered_data = data[data > 10]
print("\n条件に基づく選択:", filtered_data)

# 全要素の合計
total_sum = data.sum()
print("全要素の合計:", total_sum)

出力例(実行環境によって時間は異なります):

--- 6. NumPyのベクトル化演算の例 (パフォーマンス比較) ---
NumPy vectorized time: 0.000720 seconds

条件に基づく選択: [20 15 30]
全要素の合計: 80

(Pythonのループで同じ処理を行うと、この例では数秒かかることがあります。)



NumPyは、Pythonのループ処理の遅さを克服するために設計されています。したがって、可能な限りPythonのforループによる明示的なイテレーションを避け、NumPyが提供する最適化された機能を利用することが推奨されます。

ベクトル化 (Vectorization)

NumPyの最も強力な特徴であり、イテレーションの究極の代替手段です。配列全体または配列のサブセットに対して一度に操作を適用することを指します。これにより、内部的にはC言語などの高速なコードが実行され、Pythonループのオーバーヘッドがなくなります。

どのような時に使うか

  • 配列の統計量を計算する場合(合計、平均、最大値など)。
  • 配列の要素を比較したり、条件に基づいて要素を選択したりする場合。
  • 配列の各要素に同じ数学的演算(加算、乗算、平方根など)を適用する場合。

コード例

import numpy as np

arr = np.arange(1, 6) # [1, 2, 3, 4, 5]

print("--- 1. ベクトル化 ---")

# 各要素を2倍にする
# イテレーションの代わりに: for x in arr: x * 2
result_multiply = arr * 2
print(f"各要素を2倍: {result_multiply}")

# 各要素に10を加算する
# イテレーションの代わりに: for x in arr: x + 10
result_add = arr + 10
print(f"各要素に10を加算: {result_add}")

# 条件に基づいて要素を選択する
# イテレーションの代わりに: [x for x in arr if x > 3]
result_filter = arr[arr > 3]
print(f"3より大きい要素: {result_filter}")

# 配列の全要素の合計を計算する
# イテレーションの代わりに: total = 0; for x in arr: total += x
result_sum = arr.sum()
print(f"全要素の合計: {result_sum}")

# 複数の配列間の要素ごとの演算(ブロードキャストも含む)
arr_a = np.array([[1, 2], [3, 4]])
arr_b = np.array([10, 20]) # ブロードキャストされる
result_element_wise_add = arr_a + arr_b
print(f"要素ごとの加算(ブロードキャスト):\n{result_element_wise_add}")
--- 1. ベクトル化 ---
各要素を2倍: [ 2  4  6  8 10]
各要素に10を加算: [11 12 13 14 15]
3より大きい要素: [4 5]
全要素の合計: 15
要素ごとの加算(ブロードキャスト):
[[11 22]
 [13 24]]

ユニバーサル関数 (Universal Functions - ufuncs)

ufuncsは、NumPyが提供する要素ごとの演算を行う関数の総称です。これらはC言語で実装されており、ベクトル化された操作を効率的に実行します。np.add, np.sqrt, np.sin, np.expなど、非常に多くのufuncsが存在します。多くの場合、算術演算子(+, -, *など)を使うと、内部的に対応するufuncが呼び出されます。

どのような時に使うか

  • ベクトル化と全く同じ状況で、特に非線形な数学関数を適用する場合。

コード例

import numpy as np

arr = np.array([1, 4, 9, 16])

print("\n--- 2. ユニバーサル関数 (ufuncs) ---")

# 各要素の平方根を計算する
result_sqrt = np.sqrt(arr)
print(f"各要素の平方根: {result_sqrt}")

# 各要素の自然対数を計算する
result_log = np.log(arr)
print(f"各要素の自然対数: {result_log}")

# 複数の配列の要素ごとの最大値を計算する
arr1 = np.array([1, 5, 2])
arr2 = np.array([4, 3, 6])
result_maximum = np.maximum(arr1, arr2)
print(f"要素ごとの最大値: {result_maximum}")
--- 2. ユニバーサル関数 (ufuncs) ---
各要素の平方根: [1. 2. 3. 4.]
各要素の自然対数: [0.         1.38629436 2.19722458 2.77258872]
要素ごとの最大値: [4 5 6]

apply_along_axis / apply_over_axes

これらの関数は、特定の軸(行または列など)に沿って任意の関数を適用したい場合に便利です。ufuncでは直接できないような、より複雑なカスタム関数を適用する際に役立ちます。ただし、内部的にはPythonループが走るため、パフォーマンスはベクトル化されたufuncに劣ります。

どのような時に使うか

  • 配列の特定の軸に沿って、NumPyに直接対応するufuncがないカスタム関数を適用したい場合。

コード例

import numpy as np

arr_2d = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

def custom_sum_plus_one(x):
    """配列の合計に1を加えるカスタム関数"""
    return x.sum() + 1

print("\n--- 3. apply_along_axis ---")

# 各行の合計に1を加える (軸=1は行方向)
result_rows = np.apply_along_axis(custom_sum_plus_one, 1, arr_2d)
print(f"各行の合計+1: {result_rows}")

# 各列の合計に1を加える (軸=0は列方向)
result_cols = np.apply_along_axis(custom_sum_plus_one, 0, arr_2d)
print(f"各列の合計+1: {result_cols}")
--- 3. apply_along_axis ---
各行の合計+1: [ 7 16 25]
各列の合計+1: [13 16 19]

np.where / ブールインデックス参照

条件に基づいて配列の要素を選択したり、値を置き換えたりする場合に非常に効率的です。Pythonのif/else文を含むループの代替となります。

どのような時に使うか

  • 条件付きロジックをベクトル化したい場合。
  • 配列内の特定の条件を満たす要素を見つけたり、変更したりする場合。

コード例

import numpy as np

arr = np.array([10, 5, 20, 15, 30])

print("\n--- 4. np.where / ブールインデックス参照 ---")

# 15より大きい要素を2倍、それ以外はそのまま
# イテレーションの代わりに: for x in arr: if x > 15: print(x * 2) else: print(x)
result_conditional = np.where(arr > 15, arr * 2, arr)
print(f"条件に基づいて値変更: {result_conditional}")

# 偶数のみを選択
# イテレーションの代わりに: [x for x in arr if x % 2 == 0]
result_even = arr[arr % 2 == 0]
print(f"偶数のみ: {result_even}")
--- 4. np.where / ブールインデックス参照 ---
条件に基づいて値変更: [10  5 40 15 60]
偶数のみ: [10 20 30]

np.vectorize

Pythonの既存の関数をNumPyのufuncのように適用したい場合にnp.vectorizeを使用できます。ただし、これは単にPythonのループを隠蔽するものであり、根本的なパフォーマンス改善(C言語レベルの最適化)を提供するわけではありません。コードの見た目をNumPyらしく保ちたい場合に役立ちます。

どのような時に使うか

  • コードの可読性を高めたい場合。
  • Pythonで書かれたカスタム関数をNumPy配列の各要素に適用したいが、apply_along_axisのように軸を指定する必要がない場合。

コード例

import numpy as np

def my_custom_func(x, y):
    """xが偶数ならx+y、奇数ならx-yを返す関数"""
    if x % 2 == 0:
        return x + y
    else:
        return x - y

# Python関数をベクトル化
vectorized_func = np.vectorize(my_custom_func)

arr1 = np.array([1, 2, 3, 4])
arr2 = np.array([10, 20, 30, 40])

print("\n--- 5. np.vectorize ---")
result_vectorized = vectorized_func(arr1, arr2)
print(f"ベクトル化されたカスタム関数の結果: {result_vectorized}")
--- 5. np.vectorize ---
ベクトル化されたカスタム関数の結果: [-9 22 -27 44]

NumPyで配列の要素を処理する際は、「ベクトル化」が第一の選択肢であるべきです。ほとんどの一般的な操作はNumPyの組み込み機能やufuncでカバーできます。より複雑なロジックやカスタム関数が必要な場合は、np.whereapply_along_axisnp.vectorizeなどを検討しますが、これらはPythonの内部ループを伴うため、パフォーマンスのトレードオフが発生することを理解しておく必要があります。