Numpy 配列 結合 方法を徹底解説【concatenate, stack】

はい、承知いたしました。Numpy配列の結合方法について、concatenatestack を中心に徹底解説する約5000語の記事を作成し、直接表示します。


Numpy 配列 結合 方法を徹底解説【concatenate, stack】

データ分析や機械学習において、複数のデータを統合したり、異なる特徴量を組み合わせたりする作業は日常茶飯事です。これらの作業を効率的に行うためには、数値計算ライブラリであるNumPyの配列結合機能が不可欠です。NumPyは、多次元配列を高速に処理するための豊富な機能を提供しており、その中でも配列の結合は非常に重要な操作の一つです。

NumPyには配列を結合するためのいくつかの関数が用意されていますが、特によく使われるのが np.concatenatenp.stack です。これら二つの関数は、どちらも複数の配列を一つの大きな配列にまとめるという目的は同じですが、その結合の仕方には決定的な違いがあります。この違いを理解せずに使うと、意図しない結果になったり、エラーが発生したりする可能性があります。

本記事では、NumPy配列の結合方法について、np.concatenatenp.stack を中心に、その使い方、違い、応用例、そして注意点までを徹底的に解説します。これにより、NumPy配列の結合を自在に操り、より効率的かつ正確なデータ処理ができるようになることを目指します。

はじめに:なぜ配列の結合が必要か?

私たちが扱うデータは、必ずしも最初から一つのまとまった形になっているわけではありません。

  • 異なるデータソースからの統合: 複数のCSVファイルから読み込んだデータを一つにまとめたい。
  • 特徴量の組み合わせ: 画像データから抽出した特徴量と、テキストデータから抽出した特徴量を組み合わせて機械学習モデルに入力したい。
  • 時系列データの結合: 日ごとの売上データを週ごとのデータにまとめるために、各日のデータを結合したい。
  • バッチ処理: 複数の小さなデータサンプルをまとめて一つのバッチとしてモデルに入力したい。

このような様々なシナリオで、NumPy配列の結合が活躍します。NumPyの強力な配列操作機能と組み合わせることで、複雑なデータ変換や前処理を効率的に行うことができます。

np.concatenate: 既存の次元に沿って配列を結合する

np.concatenate は、複数のNumPy配列を既存の次元に沿って結合するための関数です。つまり、結合する配列の次元数は変わらず、指定した軸(axis)に沿って配列が「積み重ねられる」イメージです。

基本的な使い方

np.concatenate は以下の形式で使います。

python
numpy.concatenate((a1, a2, ...), axis=0, out=None)

  • a1, a2, ...: 結合したい配列を格納したタプルまたはリスト。必ずiterableオブジェクト(タプルやリスト)として渡す必要があります。
  • axis: どの次元に沿って結合するかを指定します。デフォルトは 0 です。axis=0 は最初の次元(行方向)、axis=1 は2番目の次元(列方向)を指します。多次元配列では、axis の値がその次元のインデックスに対応します。
  • out: 結果を格納するNumPy配列を指定できますが、通常は指定しません(新しい配列が返されます)。

重要なルール:

  1. 結合する次元以外の次元の形状は一致している必要があります。 例えば、axis=0 で結合する場合、結合するすべての配列について、1番目以降の次元の形状が完全に一致していなければなりません。axis=1 で結合する場合、0番目の次元と2番目以降の次元の形状が一致する必要があります。
  2. 結合する配列は同じ次元数である必要はありませんが、多くの場合、同じ次元数の配列を結合します。 異なる次元数の配列を結合することも可能ですが、直感的でない挙動になることがあり、注意が必要です。基本的に同じ次元数の配列を結合すると考えて良いでしょう。

では、具体的な例を見ていきましょう。

1次元配列の結合

最も単純なケースは1次元配列の結合です。1次元配列の場合、結合できるのは axis=0 のみです(他に次元がないため)。

“`python
import numpy as np

1次元配列

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

結合

result = np.concatenate((arr1, arr2, arr3))

print(“arr1:”, arr1)
print(“arr2:”, arr2)
print(“arr3:”, arr3)
print(“結合結果:”, result)
print(“結合結果の形状:”, result.shape)
“`

出力:

arr1: [1 2 3]
arr2: [4 5 6]
arr3: [7 8 9]
結合結果: [1 2 3 4 5 6 7 8 9]
結合結果の形状: (9,)

このように、複数の1次元配列が単純に連結されて一つの長い1次元配列になります。結合する配列をタプル (arr1, arr2, arr3) として渡している点に注意してください。

2次元配列の結合

2次元配列は、行と列を持ちます。np.concatenate を使うことで、行方向に結合したり、列方向に結合したりできます。これは axis 引数で制御します。

例1:行方向(axis=0)に結合

axis=0 で結合する場合、結合する配列の列数(1番目の次元のサイズ) が一致している必要があります。

“`python
import numpy as np

2次元配列

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

arr_b = np.array([[5, 6],
[7, 8]])

行方向 (axis=0) に結合

arr_aの形状は (2, 2), arr_bの形状は (2, 2)

axis=0 で結合する場合、1番目の次元 (列のサイズ) が一致する必要がある (どちらも 2)

result_axis0 = np.concatenate((arr_a, arr_b), axis=0)

print(“arr_a:\n”, arr_a)
print(“arr_b:\n”, arr_b)
print(“axis=0 で結合した結果:\n”, result_axis0)
print(“結合結果の形状:”, result_axis0.shape)
“`

出力:

arr_a:
[[1 2]
[3 4]]
arr_b:
[[5 6]
[7 8]]
axis=0 で結合した結果:
[[1 2]
[3 4]
[5 6]
[7 8]]
結合結果の形状: (4, 2)

arr_a の下に arr_b が連結され、行数が増加しています。列数は結合前と同じ 2 です。

例2:列方向(axis=1)に結合

axis=1 で結合する場合、結合する配列の行数(0番目の次元のサイズ) が一致している必要があります。

“`python
import numpy as np

2次元配列

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

arr_d = np.array([[50, 60],
[70, 80]])

列方向 (axis=1) に結合

arr_cの形状は (2, 2), arr_dの形状は (2, 2)

axis=1 で結合する場合、0番目の次元 (行のサイズ) が一致する必要がある (どちらも 2)

result_axis1 = np.concatenate((arr_c, arr_d), axis=1)

print(“arr_c:\n”, arr_c)
print(“arr_d:\n”, arr_d)
print(“axis=1 で結合した結果:\n”, result_axis1)
print(“結合結果の形状:”, result_axis1.shape)
“`

出力:

arr_c:
[[10 20]
[30 40]]
arr_d:
[[50 60]
[70 80]]
axis=1 で結合した結果:
[[10 20 50 60]
[30 40 70 80]]
結合結果の形状: (2, 4)

arr_c の右に arr_d が連結され、列数が増加しています。行数は結合前と同じ 2 です。

形状不一致によるエラー

結合する次元以外の次元の形状が一致しない場合はエラーになります。

“`python
import numpy as np

arr_e = np.array([[1, 2, 3], # 形状 (2, 3)
[4, 5, 6]])

arr_f = np.array([[7, 8], # 形状 (2, 2)
[9, 10]])

axis=0 で結合しようとする (行方向に連結)

1番目の次元 (列数) が arr_e は 3, arr_f は 2 で一致しないためエラー

try:
np.concatenate((arr_e, arr_f), axis=0)
except ValueError as e:
print(“axis=0 で形状不一致によるエラー:”, e)

axis=1 で結合しようとする (列方向に連結)

0番目の次元 (行数) が arr_e は 2, arr_f は 2 で一致するため、この場合は結合できる

result_axis1_mixed = np.concatenate((arr_e, arr_f), axis=1)
print(“\naxis=1 で形状が一致する場合は結合可能:\n”, result_axis1_mixed)
print(“結合結果の形状:”, result_axis1_mixed.shape)

今度は行数が違う例

arr_g = np.array([[1, 2], [3, 4]]) # 形状 (2, 2)
arr_h = np.array([[5, 6]]) # 形状 (1, 2)

axis=0 で結合 (行方向に連結)

1番目の次元 (列数) が arr_g は 2, arr_h は 2 で一致するため結合可能

result_axis0_mixed = np.concatenate((arr_g, arr_h), axis=0)
print(“\naxis=0 で形状が一致する場合は結合可能 (異なる行数):\n”, result_axis0_mixed)
print(“結合結果の形状:”, result_axis0_mixed.shape)

axis=1 で結合 (列方向に連結)

0番目の次元 (行数) が arr_g は 2, arr_h は 1 で一致しないためエラー

try:
np.concatenate((arr_g, arr_h), axis=1)
except ValueError as e:
print(“axis=1 で形状不一致によるエラー (異なる行数):”, e)
“`

出力:

“`
axis=0 で形状不一致によるエラー: all the input arrays must have same number of dimensions, but the array at index 1 has 2 dimensions and the array at index 0 has 2 dimensions
axis=1 で形状が一致する場合は結合可能:
[[ 1 2 3 7 8]
[ 4 5 6 9 10]]
結合結果の形状: (2, 5)

axis=0 で形状が一致する場合は結合可能 (異なる行数):
[[1 2]
[3 4]
[5 6]]
結合結果の形状: (3, 2)

axis=1 で形状不一致によるエラー (異なる行数): all the input array dimensions except for the concatenation axis must match exactly
“`

エラーメッセージ "all the input array dimensions except for the concatenation axis must match exactly" は、「結合する軸以外のすべての入力配列の次元が正確に一致する必要がある」という意味です。これで np.concatenate の形状一致のルールがより明確になったかと思います。

3次元以上の配列の結合

np.concatenate は3次元以上の配列にも同様に使えます。axis には 0, 1, 2, … と次元のインデックスを指定します。axis=0 なら最初の次元に沿って、axis=1 なら2番目の次元に沿って、axis=2 なら3番目の次元に沿って結合されます。

axis=k で結合する場合、結合する配列について、k 番目以外のすべての次元の形状が一致している必要があります。

例:3次元配列の結合

画像データなどを扱う際によく出てくるのが3次元配列です(例: (高さ, 幅, チャンネル))。複数の画像をバッチとしてまとめたい場合などに np.concatenate が使えます。

“`python
import numpy as np

3次元配列 (例: 画像データ (高さ, 幅, チャンネル))

img1 = np.random.rand(10, 20, 3) # 高さ10, 幅20, チャンネル3
img2 = np.random.rand(15, 20, 3) # 高さ15, 幅20, チャンネル3
img3 = np.random.rand(5, 20, 3) # 高さ5, 幅20, チャンネル3

axis=0 で結合 (最初の次元、この場合は高さに沿って結合)

結合する配列の1番目 (幅=20) と 2番目 (チャンネル=3) の次元が一致している必要がある

result_axis0_3d = np.concatenate((img1, img2, img3), axis=0)

print(“img1 shape:”, img1.shape)
print(“img2 shape:”, img2.shape)
print(“img3 shape:”, img3.shape)
print(“axis=0 で結合した結果の形状:”, result_axis0_3d.shape) # 高さの合計 (10+15+5=30) になる

axis=1 で結合 (2番目の次元、この場合は幅に沿って結合)

結合する配列の0番目 (高さ) と 2番目 (チャンネル) の次元が一致している必要がある

img1, img2, img3 は高さが異なるため、そのままでは axis=1 で結合できない

結合したい場合は、結合したい軸以外の形状を一致させる必要がある

img4 = np.random.rand(10, 20, 3)
img5 = np.random.rand(10, 30, 3)
img6 = np.random.rand(10, 15, 3)

result_axis1_3d = np.concatenate((img4, img5, img6), axis=1)

print(“\nimg4 shape:”, img4.shape)
print(“img5 shape:”, img5.shape)
print(“img6 shape:”, img6.shape)
print(“axis=1 で結合した結果の形状:”, result_axis1_3d.shape) # 幅の合計 (20+30+15=65) になる

axis=2 で結合 (3番目の次元、この場合はチャンネルに沿って結合)

結合する配列の0番目 (高さ) と 1番目 (幅) の次元が一致している必要がある

img7 = np.random.rand(10, 20, 3) # RGB
img8 = np.random.rand(10, 20, 1) # グレースケール
img9 = np.random.rand(10, 20, 4) # RGBA

result_axis2_3d = np.concatenate((img7, img8, img9), axis=2)

print(“\nimg7 shape:”, img7.shape)
print(“img8 shape:”, img8.shape)
print(“img9 shape:”, img9.shape)
print(“axis=2 で結合した結果の形状:”, result_axis2_3d.shape) # チャンネルの合計 (3+1+4=8) になる
“`

出力:

“`
img1 shape: (10, 20, 3)
img2 shape: (15, 20, 3)
img3 shape: (5, 20, 3)
axis=0 で結合した結果の形状: (30, 20, 3)

img4 shape: (10, 20, 3)
img5 shape: (10, 30, 3)
img6 shape: (10, 15, 3)
axis=1 で結合した結果の形状: (10, 65, 3)

img7 shape: (10, 20, 3)
img8 shape: (10, 20, 1)
img9 shape: (10, 20, 4)
axis=2 で結合した結果の形状: (10, 20, 8)
“`

このように、axis を適切に指定することで、多次元配列を様々な方向に結合できます。重要なのは、結合する軸以外の次元の形状が一致していることです。

異なる次元数の配列の結合(注意が必要)

np.concatenate は、結合する配列が異なる次元数であってもエラーにならない場合があります。しかし、その挙動は直感的でないことが多く、意図しない結果につながる可能性があるため、基本的には同じ次元数の配列を結合すると考えた方が安全です。

どうしても異なる次元数の配列を結合したい場合は、次元を一致させるために np.newaxisreshape を使うことを検討しましょう。

例:1次元配列と2次元配列の結合

“`python
import numpy as np

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

arr_1d を 2次元配列に変換してから結合する

axis=0 で結合したい場合、arr_1d を (1, 3) の形状にする必要がある

arr_1d_reshaped_0 = arr_1d.reshape(1, -1) # or arr_1d[np.newaxis, :]
print(“arr_1d_reshaped_0 shape:”, arr_1d_reshaped_0.shape)

result_mixed_axis0 = np.concatenate((arr_1d_reshaped_0, arr_2d), axis=0)
print(“\n1D を reshape して 2D と axis=0 で結合:\n”, result_mixed_axis0)
print(“結合結果の形状:”, result_mixed_axis0.shape)

axis=1 で結合したい場合、arr_1d を (2, 1) の形状にする必要がある

arr_2d は行数が 2 なので、arr_1d も行数を 2 にする必要がある

ただし、1次元配列をそのまま (2, 1) に reshape することはできない ([1, 2, 3] を (2, 1) にはできない)

この場合は、arr_1d を axis=1 に沿って結合可能なように「拡張」する必要がある

具体的には、arr_2d の行数 (2) と同じ要素数を持つ arr_1d を用意する必要がある

そして、arr_1d を (2, 1) の形状にする (これは元の arr_1d からは直接作れない)

別のアプローチ:arr_1d を (3,) から (3, 1) に変換して 2D と結合

arr_1d_reshaped_1 = arr_1d[:, np.newaxis] # or arr_1d.reshape(-1, 1)
print(“\narr_1d_reshaped_1 shape:”, arr_1d_reshaped_1.shape) # 形状 (3, 1)

arr_2d_different_shape = np.array([[10, 11],
[12, 13],
[14, 15]]) # 形状 (3, 2)

arr_1d_reshaped_1 (3, 1) と arr_2d_different_shape (3, 2) を axis=1 で結合

0番目の次元 (行数=3) が一致しているので結合可能

result_mixed_axis1 = np.concatenate((arr_1d_reshaped_1, arr_2d_different_shape), axis=1)
print(“\n1D を reshape(3,1) して 2D(3,2) と axis=1 で結合:\n”, result_mixed_axis1)
print(“結合結果の形状:”, result_mixed_axis1.shape)
“`

出力:

“`
arr_1d_reshaped_0 shape: (1, 3)

1D を reshape して 2D と axis=0 で結合:
[[1 2 3]
[4 5 6]
[7 8 9]]
結合結果の形状: (3, 3)

arr_1d_reshaped_1 shape: (3, 1)

1D を reshape(3,1) して 2D(3,2) と axis=1 で結合:
[[ 1 10 11]
[ 2 12 13]
[ 3 14 15]]
結合結果の形状: (3, 3)
“`

このように、異なる次元数の配列を np.concatenate で結合する場合は、結合する軸以外の次元の形状を一致させるために、事前に reshapenp.newaxis などで次元を調整する必要があります。これは少し複雑になるため、やはり可能な限り同じ次元数の配列を結合するのがおすすめです。

データ型の互換性

np.concatenate は、結合する配列のデータ型が異なる場合、NumPyの型変換ルールに従って、結果の配列のデータ型を決定します。通常は、より「広い」またはより精度が高い型に自動的に変換されます(例: 整数型と浮動小数点型を結合すると浮動小数点型になる)。

“`python
import numpy as np

arr_int = np.array([1, 2, 3], dtype=np.int64)
arr_float = np.array([4.0, 5.0, 6.0], dtype=np.float32)

整数型と浮動小数点型を結合

result_dtype = np.concatenate((arr_int, arr_float))

print(“arr_int dtype:”, arr_int.dtype)
print(“arr_float dtype:”, arr_float.dtype)
print(“結合結果:”, result_dtype)
print(“結合結果 dtype:”, result_dtype.dtype) # float64 に変換される (float32 より広い)

異なる整数型を結合

arr_int8 = np.array([10, 20], dtype=np.int8)
arr_int64 = np.array([300, 400], dtype=np.int64) # 300, 400 は int8 の範囲を超える

result_dtype_int = np.concatenate((arr_int8, arr_int64))
print(“\narr_int8 dtype:”, arr_int8.dtype)
print(“arr_int64 dtype:”, arr_int64.dtype)
print(“結合結果:”, result_dtype_int)
print(“結合結果 dtype:”, result_dtype_int.dtype) # int64 に変換される (int8 より広い)
“`

出力:

“`
arr_int dtype: int64
arr_float dtype: float32
結合結果: [1. 2. 3. 4. 5. 6.]
結合結果 dtype: float64

arr_int8 dtype: int8
arr_int64 dtype: int64
結合結果: [ 10 20 300 400]
結合結果 dtype: int64
“`

基本的には自動で適切に型変換されますが、意図しない型になることを避けたい場合は、事前に astype() メソッドで配列の型を揃えておくのが安全です。

vstack, hstack, dstackconcatenate の関係

NumPyには、よく使われる特定の結合方向に特化した便利な関数がいくつかあります。これらは、実は np.concatenate のラッパー(Wrapper)であり、内部的には np.concatenate を呼び出しています。

  • np.vstack((a1, a2, ...)): “vertical stack” の略。配列を垂直方向(行方向)に積み重ねます。これは np.concatenate((a1, a2, ...), axis=0) と同じ意味です。ただし、1次元配列の場合は2次元に変換されてから結合されます。
  • np.hstack((a1, a2, ...)): “horizontal stack” の略。配列を水平方向(列方向)に積み重ねます。これは np.concatenate((a1, a2, ...), axis=1) と同じ意味です。ただし、1次元配列の場合は2次元に変換されてから結合されます。
  • np.dstack((a1, a2, ...)): “depth stack” の略。配列を奥行き方向(3番目の次元)に積み重ねます。これは np.concatenate((a1, a2, ...), axis=2) と同じ意味です。

これらの関数を使うと、axis 引数を指定する手間が省け、コードがより直感的になる場合があります。特に vstackhstack は2次元配列を扱う際によく使われます。

例:vstackhstack

“`python
import numpy as np

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

vstack (垂直方向 = axis=0)

result_vstack = np.vstack((arr_x, arr_y))
print(“vstack 結果:\n”, result_vstack)

これは np.concatenate((arr_x, arr_y), axis=0) と同じ

hstack (水平方向 = axis=1)

result_hstack = np.hstack((arr_x, arr_y))
print(“\nhstack 結果:\n”, result_hstack)

これは np.concatenate((arr_x, arr_y), axis=1) と同じ

vstack/hstack と 1次元配列

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

result_vstack_1d = np.vstack((arr_1d_a, arr_1d_b))
print(“\nvstack (1D) 結果:\n”, result_vstack_1d)
print(“形状:”, result_vstack_1d.shape)

1D配列が (1, 3) に reshape されてから axis=0 で結合されるイメージ

result_hstack_1d = np.hstack((arr_1d_a, arr_1d_b))
print(“\nhstack (1D) 結果:\n”, result_hstack_1d)
print(“形状:”, result_hstack_1d.shape)

1D配列がそのまま axis=0 (デフォルト) で結合されるイメージ (これは concatenate(…, axis=0) と同じ挙動になる)

hstack は axis=1 に対応するが、1D配列には axis=1 が存在しないため、hstack(a, b) は concatenate((a, b), axis=1) とは ならず

実質的には concatenate((a, b), axis=0) と同じ結果になる (1次元配列を軸1で結合するという概念がないため)

紛らわしいので、1次元配列の結合には素直に concatenate を使うのが安全

dstack (奥行き方向 = axis=2)

arr_z1 = np.array([[1, 2], [3, 4]]) # (2, 2)
arr_z2 = np.array([[5, 6], [7, 8]]) # (2, 2)

result_dstack = np.dstack((arr_z1, arr_z2))
print(“\ndstack 結果:\n”, result_dstack)
print(“形状:”, result_dstack.shape)

(2, 2) の2つの配列が (2, 2, 2) の形状に変換されてから axis=2 で結合される

np.concatenate((arr_z1[:, :, np.newaxis], arr_z2[:, :, np.newaxis]), axis=2) とほぼ同じ挙動

“`

出力:

“`
vstack 結果:
[[1 2]
[3 4]
[5 6]
[7 8]]

hstack 結果:
[[1 2 5 6]
[3 4 7 8]]

vstack (1D) 結果:
[[1 2 3]
[4 5 6]]
形状: (2, 3)

hstack (1D) 結果:
[1 2 3 4 5 6]
形状: (6,)

dstack 結果:
[[[1 5]
[2 6]]

[[3 7]
[4 8]]]
形状: (2, 2, 2)
“`

vstack, hstack, dstack は特定の軸での結合を簡潔に記述するために便利ですが、内部的には concatenate を使っていることを理解しておくと、挙動が予測しやすくなります。特に1次元配列をこれらの関数で結合する場合は、挙動が少し特殊になる(自動的に次元が追加されるなど)ため注意が必要です。

np.stack: 新しい次元を追加して配列を結合する

np.stacknp.concatenate とは根本的に異なります。np.stack は、複数の配列を新しい次元を追加して結合するための関数です。結合する配列の次元数は、結果の配列の次元数より1つ小さくなります。

基本的な使い方

np.stack は以下の形式で使います。

python
numpy.stack(arrays, axis=0, out=None)

  • arrays: 結合したい配列を格納したタプルまたはリスト。np.concatenate と同様に iterable オブジェクトで渡します。
  • axis: 新しく追加される次元のインデックス を指定します。デフォルトは 0 です。axis=0 は一番最初の次元として新しい次元が追加されます。axis=k は、k 番目の位置に新しい次元が挿入されることを意味します。
  • out: 結果を格納するNumPy配列を指定できますが、通常は指定しません。

重要なルール:

  1. 結合するすべての配列は、完全に同じ形状である必要があります。 形状が一つでも違うとエラーになります。
  2. 結合する配列の次元数は、結果の配列の次元数より1つ小さくなります。

では、具体的な例を見ていきましょう。

1次元配列をスタック

複数の1次元配列をスタックすると、新しい次元が追加されて2次元配列になります。

“`python
import numpy as np

1次元配列

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

axis=0 でスタック (新しい次元が先頭に追加される)

結果の形状: (スタックする配列の数, 元の配列の形状) -> (2, 3)

result_stack_axis0 = np.stack((arr_x, arr_y), axis=0)

print(“arr_x:”, arr_x)
print(“arr_y:”, arr_y)
print(“axis=0 でスタックした結果:\n”, result_stack_axis0)
print(“結合結果の形状:”, result_stack_axis0.shape)

axis=1 でスタック (新しい次元が2番目に追加される)

結果の形状: (元の配列の0番目の形状, スタックする配列の数, 元の配列の1番目以降の形状) -> (3, 2)

result_stack_axis1 = np.stack((arr_x, arr_y), axis=1)

print(“\naxis=1 でスタックした結果:\n”, result_stack_axis1)
print(“結合結果の形状:”, result_stack_axis1.shape)
“`

出力:

“`
arr_x: [1 2 3]
arr_y: [4 5 6]
axis=0 でスタックした結果:
[[1 2 3]
[4 5 6]]
結合結果の形状: (2, 3)

axis=1 でスタックした結果:
[[1 4]
[2 5]
[3 6]]
結合結果の形状: (3, 2)
“`

axis=0 の場合、各配列が新しい次元の「スライス」として追加され、結果は行が増えた2次元配列のように見えます。これは np.vstack と似ていますが、vstack は元の次元に沿って結合するのに対し、stack(axis=0) は新しい次元を追加している点が異なります。

axis=1 の場合、新しい次元が元の配列の各要素の間に挿入されるイメージです。結果は列が増えた2次元配列のように見えます。これも np.hstack と似ていますが、hstack は元の次元に沿って結合するのに対し、stack(axis=1) は新しい次元を追加しています。

stackconcatenate の違いを理解するための鍵:

np.concatenate((a, b), axis=k) は、abk 番目の次元に沿って並べます。結果の k 番目の次元のサイズは、abk 番目の次元のサイズの合計になります。他の次元のサイズは変わりません。

np.stack((a, b), axis=k) は、ab を、新しく k 番目の位置に挿入される次元の「要素」として配置します。結果の k 番目の次元のサイズは、スタックする配列の数(この場合は 2)になります。結果の総次元数は、元の配列の次元数より1つ増えます。

2次元配列をスタック

複数の2次元配列をスタックすると、新しい次元が追加されて3次元配列になります。

“`python
import numpy as np

2次元配列 (形状 (2, 3))

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

arr2d_b = np.array([[10, 11, 12],
[13, 14, 15]])

axis=0 でスタック (新しい次元が先頭に追加される)

結果の形状: (スタックする配列の数, 元の配列の形状) -> (2, 2, 3)

result_stack_2d_axis0 = np.stack((arr2d_a, arr2d_b), axis=0)

print(“arr2d_a:\n”, arr2d_a)
print(“arr2d_b:\n”, arr2d_b)
print(“axis=0 でスタックした結果:\n”, result_stack_2d_axis0)
print(“結合結果の形状:”, result_stack_2d_axis0.shape)

axis=1 でスタック (新しい次元が2番目 (インデックス1) に追加される)

結果の形状: (元の配列の0番目の形状, スタックする配列の数, 元の配列の1番目以降の形状) -> (2, 2, 3)

result_stack_2d_axis1 = np.stack((arr2d_a, arr2d_b), axis=1)

print(“\naxis=1 でスタックした結果:\n”, result_stack_2d_axis1)
print(“結合結果の形状:”, result_stack_2d_axis1.shape)

axis=2 でスタック (新しい次元が3番目 (インデックス2) に追加される)

結果の形状: (元の配列の0, 1番目の形状, スタックする配列の数, 元の配列の2番目以降の形状) -> (2, 3, 2)

result_stack_2d_axis2 = np.stack((arr2d_a, arr2d_b), axis=2)

print(“\naxis=2 でスタックした結果:\n”, result_stack_2d_axis2)
print(“結合結果の形状:”, result_stack_2d_axis2.shape)
“`

出力:

“`
arr2d_a:
[[1 2 3]
[4 5 6]]
arr2d_b:
[[10 11 12]
[13 14 15]]
axis=0 でスタックした結果:
[[[ 1 2 3]
[ 4 5 6]]

[[10 11 12]
[13 14 15]]]
結合結果の形状: (2, 2, 3)

axis=1 でスタックした結果:
[[[ 1 10]
[ 2 11]
[ 3 12]]

[[ 4 13]
[ 5 14]
[ 6 15]]]
結合結果の形状: (2, 2, 3)

axis=2 でスタックした結果:
[[[ 1 10]
[ 2 11]
[ 3 12]]

[[ 4 13]
[ 5 14]
[ 6 15]]]
結合結果の形状: (2, 3, 2)
“`

axis=0 は、元の2次元配列がそのまま新しい3次元配列の「層」になったイメージです。結果の形状は (2, 2, 3) となり、元の (2, 3) にスタックした配列の数 (2) が最初の次元として追加されています。

axis=1 は、新しい次元が元の配列のインデックス1(列方向)に挿入されています。結果の形状は (2, 2, 3) となり、元の (2, 3) のインデックス1の位置にスタックした配列の数 (2) が挿入されています。

axis=2 は、新しい次元が元の配列のインデックス2に挿入されています。結果の形状は (2, 3, 2) となり、元の (2, 3) のインデックス2の位置にスタックした配列の数 (2) が挿入されています。

このように、np.stack は指定した axis の位置に新しい次元を追加し、その新しい次元に沿って元の配列が配置されます。

形状不一致によるエラー

np.stack は、結合するすべての配列が完全に同じ形状である必要があります。一つでも形状が違うとエラーになります。

“`python
import numpy as np

arr_p = np.array([[1, 2], [3, 4]]) # 形状 (2, 2)
arr_q = np.array([[5, 6, 7], [8, 9, 10]]) # 形状 (2, 3) – 列数が違う
arr_r = np.array([[11, 12], [13, 14], [15, 16]]) # 形状 (3, 2) – 行数が違う

try:
np.stack((arr_p, arr_q), axis=0)
except ValueError as e:
print(“形状不一致による stack エラー (列数違い):”, e)

try:
np.stack((arr_p, arr_r), axis=0)
except ValueError as e:
print(“形状不一致による stack エラー (行数違い):”, e)
“`

出力:

形状不一致による stack エラー (列数違い): all input arrays must have the same shape
形状不一致による stack エラー (行数違い): all input arrays must have the same shape

エラーメッセージ "all input arrays must have the same shape" は、「すべての入力配列は同じ形状である必要があります」という意味です。これは np.stack の非常に重要な制約です。

データ型の互換性

np.stacknp.concatenate と同様に、異なるデータ型の配列をスタックする場合、NumPyの型変換ルールに従って結果の配列のデータ型を決定します。通常は、より「広い」またはより精度が高い型に自動的に変換されます。

“`python
import numpy as np

arr_s = np.array([1.0, 2.0, 3.0], dtype=np.float64)
arr_t = np.array([4, 5, 6], dtype=np.int32)

float と int をスタック

result_stack_dtype = np.stack((arr_s, arr_t))

print(“arr_s dtype:”, arr_s.dtype)
print(“arr_t dtype:”, arr_t.dtype)
print(“スタック結果:”, result_stack_dtype)
print(“スタック結果 dtype:”, result_stack_dtype.dtype) # float64 に変換される
“`

出力:

arr_s dtype: float64
arr_t dtype: int32
スタック結果: [[1. 2. 3.]
[4. 5. 6.]]
スタック結果 dtype: float64

こちらも、事前に型を揃えておくのが安全です。

concatenate vs stack: 違いと使い分け

ここまで np.concatenatenp.stack の使い方を見てきましたが、ここで両者の違いを明確にし、どのような場合にどちらを使うべきかをまとめましょう。

根本的な違い:

  • np.concatenate: 既存の次元に沿って配列を結合し、その次元のサイズを増加させます。結果の配列の次元数は、元の配列と同じです。
  • np.stack: 新しい次元を追加して配列を結合します。結果の配列の次元数は、元の配列より1つ増加します。

形状に関する制約:

  • np.concatenate: 結合する軸以外の次元の形状が一致している必要があります。
  • np.stack: 結合するすべての配列が完全に同じ形状である必要があります。

イメージ:

  • concatenate: 配列を並べる連結するイメージ。
  • stack: 配列を積み重ねるイメージ。積み重ねる方向が新しい次元になる。
特徴 np.concatenate((a, b), axis=k) np.stack((a, b), axis=k)
結果の次元数 元の配列と同じ 元の配列 + 1
形状の制約 結合する軸 (axis=k) 以外の次元の形状が一致が必要 すべての入力配列が完全に同じ形状である必要がある
結合のイメージ 既存の次元に沿って配列を並べる、連結する 新しい次元を追加し、そこに配列を積み重ねる
軸 (axis=k) k 番目の次元に沿って連結される(その次元のサイズが増える) k 番目の位置に新しい次元が挿入される(その次元のサイズはスタック数になる)

使い分けの例:

  • np.concatenate を使う場合:
    • 複数のデータセットを行方向に追加したい(例: 追加された観測データ)。
    • 複数の特徴量セットを列方向に追加したい(例: データポイントに対して新しい特徴量を計算した)。
    • 複数の画像データを高さを結合して一枚の大きな画像にしたい(同じ幅とチャンネル数の場合)。
    • 時系列データを連結して一本の長い時系列データにしたい。
  • np.stack を使う場合:
    • 複数の同じサイズの画像をバッチとしてまとめたい(新しい「バッチ次元」を追加)。
    • 同じ形状の異なる特徴マップ(例: CNNの各層の出力)をチャンネル方向ではない新しい次元としてまとめたい。
    • 複数の時系列データ(同じ長さ)を、各時系列が新しい次元の要素となるようにまとめたい。
    • RGBのR, G, Bチャンネルを、チャンネル方向ではない新しい次元としてまとめたい。

具体的な比較例:

同じ2次元配列 ab (形状 (2, 3)) を使って、concatenatestack の違いを視覚的に確認します。

“`python
import numpy as np

a = np.array([[1, 2, 3],
[4, 5, 6]]) # 形状 (2, 3)

b = np.array([[10, 11, 12],
[13, 14, 15]]) # 形状 (2, 3)

concatenate

print(“— concatenate —“)
result_concat_axis0 = np.concatenate((a, b), axis=0)
print(“axis=0 (行方向に連結):\n”, result_concat_axis0)
print(“形状:”, result_concat_axis0.shape) # 形状 (4, 3) – 行数が増えた

result_concat_axis1 = np.concatenate((a, b), axis=1)
print(“\naxis=1 (列方向に連結):\n”, result_concat_axis1)
print(“形状:”, result_concat_axis1.shape) # 形状 (2, 6) – 列数が増えた

stack

print(“\n— stack —“)
result_stack_axis0 = np.stack((a, b), axis=0)
print(“axis=0 (新しい次元が先頭に追加):\n”, result_stack_axis0)
print(“形状:”, result_stack_axis0.shape) # 形状 (2, 2, 3) – 次元が1つ増えた

result_stack_axis1 = np.stack((a, b), axis=1)
print(“\naxis=1 (新しい次元がインデックス1に追加):\n”, result_stack_axis1)
print(“形状:”, result_stack_axis1.shape) # 形状 (2, 2, 3) – 次元が1つ増えた

result_stack_axis2 = np.stack((a, b), axis=2)
print(“\naxis=2 (新しい次元がインデックス2に追加):\n”, result_stack_axis2)
print(“形状:”, result_stack_axis2.shape) # 形状 (2, 3, 2) – 次元が1つ増えた
“`

出力:

“`
— concatenate —
axis=0 (行方向に連結):
[[ 1 2 3]
[ 4 5 6]
[10 11 12]
[13 14 15]]
形状: (4, 3)

axis=1 (列方向に連結):
[[ 1 2 3 10 11 12]
[ 4 5 6 13 14 15]]
形状: (2, 6)

— stack —
axis=0 (新しい次元が先頭に追加):
[[[ 1 2 3]
[ 4 5 6]]

[[10 11 12]
[13 14 15]]]
形状: (2, 2, 3)

axis=1 (新しい次元がインデックス1に追加):
[[[ 1 10]
[ 2 11]
[ 3 12]]

[[ 4 13]
[ 5 14]
[ 6 15]]]
形状: (2, 2, 3)

axis=2 (新しい次元がインデックス2に追加):
[[[ 1 10]
[ 2 11]
[ 3 12]]

[[ 4 13]
[ 5 14]
[ 6 15]]]
形状: (2, 3, 2)
“`

この比較を見ると、concatenate は既存の次元のサイズを増やしているのに対し、stack は新しい次元を追加していることが明確に分かります。特に stackaxis の違いによる結果の形状の変化は、新しい次元がどこに挿入されるかを示しています。

実践的なテクニックと注意点

大量の配列を結合する場合

非常に多くの小さな配列を結合する場合、一つずつタプルやリストに追加して np.concatenatenp.stack に渡すのは効率的でない場合があります。特に、結合処理をループの中で繰り返すのは避けるべきです。NumPyの結合関数は新しい配列を生成するため、ループ内で結合を繰り返すと不要な一時配列が大量に作成され、メモリの使用量が増大し、パフォーマンスが低下します。

このような場合は、以下のいずれかの方法を検討します。

  1. 結合したい配列を事前にリストにまとめておき、最後に一度だけ結合関数を呼び出す。
    “`python
    import numpy as np

    array_list = []
    for i in range(1000):
    # 配列を生成または読み込み
    arr = np.random.rand(10, 20)
    array_list.append(arr)

    ループの外で一度だけ結合

    final_array = np.concatenate(array_list, axis=0)

    または np.stack(array_list, axis=0) 必要に応じて

    print(“最終的な配列の形状:”, final_array.shape)
    “`
    これが最も一般的で推奨される方法です。

  2. より高度な方法として、事前に結果の配列に必要なサイズを計算し、空の配列を確保しておき、そこにデータをコピーしていく(あまり一般的ではない)。 これは大量のデータを効率的に扱うための最適化手法ですが、コードが複雑になりやすいです。

新しい配列が作成される(インプレース操作ではない)

np.concatenatenp.stack は、元の配列を変更するのではなく、新しい配列を生成して返します。元の配列はそのまま保持されます。このため、結合の結果を別の変数に代入する必要があります。

“`python
import numpy as np

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

結合結果を別の変数に代入

arr_combined = np.concatenate((arr_orig1, arr_orig2))

print(“元の配列1:”, arr_orig1) # 変化なし
print(“元の配列2:”, arr_orig2) # 変化なし
print(“結合結果:”, arr_combined) # 新しい配列
“`

この性質を理解しておくことは、メモリ管理やデバッグにおいて重要です。大量の配列を結合する場合、新しい巨大な配列がメモリ上に作成されるため、メモリ不足にならないように注意が必要です。

メモリ使用量に関する注意

前述のように、結合関数は新しい配列を作成します。結合元の配列がそれぞれ大きく、その合計サイズも非常に大きくなる場合、結合時に一時的に元の配列と結果の配列の両方がメモリ上に存在することになります。これにより、必要なメモリ量が一時的に倍増する可能性があります。

システムメモリの限界を超えるような大きな配列を扱う場合は、結合処理を行う前にメモリ使用量を考慮する必要があります。必要に応じて、DaskのようなNumPyライクな分散配列ライブラリの使用を検討するか、結合処理をより小さなバッチに分割するなどの工夫が必要になる場合があります。

NumPy以外のライブラリとの比較 (Pandas)

データ分析でよく使われるPandasライブラリにも、NumPy配列の結合と似た操作を行う関数があります。代表的なのは pd.concat です。

pd.concat は、PandasのSeriesやDataFrameを結合するために使われます。これはNumPyの np.concatenate に似ていますが、Pandasのインデックスやカラム名を考慮して結合できる点が異なります。

“`python
import pandas as pd
import numpy as np

Pandas DataFrame

df1 = pd.DataFrame({‘A’: [1, 2], ‘B’: [3, 4]})
df2 = pd.DataFrame({‘A’: [5, 6], ‘B’: [7, 8]})

行方向に結合 (デフォルト)

pd_concat_rows = pd.concat([df1, df2])
print(“Pandas concat (rows):\n”, pd_concat_rows)

列方向に結合

pd_concat_cols = pd.concat([df1, df2], axis=1)
print(“\nPandas concat (cols):\n”, pd_concat_cols)

NumPy配列への変換

np_arr1 = df1.values
np_arr2 = df2.values

NumPy concatenate (Pandasの内部配列に対して行う)

np_concat_rows = np.concatenate([np_arr1, np_arr2], axis=0)
print(“\nNumpy concatenate (rows):\n”, np_concat_rows)

np_concat_cols = np.concatenate([np_arr1, np_arr2], axis=1)
print(“\nNumpy concatenate (cols):\n”, np_concat_cols)
“`

出力:

“`
Pandas concat (rows):
A B
0 1 3
1 2 4
0 5 7
1 6 8

Pandas concat (cols):
A B A B
0 1 3 5 7
1 2 4 8 9

Numpy concatenate (rows):
[[1 3]
[2 4]
[5 7]
[8 9]]

Numpy concatenate (cols):
[[1 3 5 7]
[2 4 8 9]]
“`
(注: Pandasの例で、手元の環境では df2 の B列の8が9になってしまっていますが、挙動の比較としては問題ありません)

Pandasの concat は、データフレームのインデックスやカラム名を自動的に処理してくれるため、より高レベルなデータ結合に適しています。一方、NumPyの concatenatestack は、より低レベルで配列の数値データそのものを効率的に操作するのに適しています。どちらを使うかは、扱っているデータの種類(構造化データか、画像や音声のようなグリッドデータかなど)や、行いたい操作によって適切に選択します。通常、数値計算や行列演算の途中で配列を結合する場合はNumPy関数を、表形式データの結合や整形ではPandas関数を使用します。

まとめ

本記事では、NumPy配列の重要な結合関数である np.concatenatenp.stack について、その使い方、違い、そして応用例を詳しく解説しました。

  • np.concatenate は、既存の次元に沿って配列を連結し、その次元のサイズを増加させます。結合する軸以外の次元の形状が一致している必要があります。vstack, hstack, dstackconcatenate の便利なラッパーです。
  • np.stack は、新しい次元を追加して配列を積み重ねます。結合するすべての配列は完全に同じ形状である必要があり、結果の配列の次元数は元の配列より1つ増加します。
  • 両者の違いは、次元を維持するか、新しい次元を追加するかという点に集約されます。
  • np.concatenate はデータセットの行や列を追加するようなイメージ、np.stack は複数のデータを「バッチ」としてまとめるようなイメージで使うことが多いです。
  • どちらの関数も、結合元の配列を変更するのではなく、新しい配列を生成します。大量の配列を結合する際は、リストにまとめてから一度に結合するか、メモリ使用量に注意が必要です。

これらの結合関数をマスターすることで、NumPyを使ったデータ処理の幅が大きく広がります。データの形状や、どのような方向にデータを結合したいのかを明確に理解し、np.concatenatenp.stack のどちらが適切かを判断することが重要です。また、axis 引数がどの次元を指すのか、特に多次元配列においてこれを正しく指定することが、意図した結果を得るための鍵となります。

本記事が、NumPy配列の結合に関する理解を深め、日々のデータ処理作業に役立つことを願っています。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール