【NumPy入門 第3回】配列操作を自由自在に!形状変更・結合・分割・条件抽出をマスターしよう

「NumPy配列の行と列の数を変えたいけど、どうすればいい?」
「複数の配列を一つにまとめたり、逆に大きな配列を小さく分けたりしたいな。」
「配列の中から、特定の条件に合う要素だけをスマートに取り出したい!」

こんにちは! NumPy探検隊、隊長のPythonistaです! 前回の第2回では、NumPy配列のインデックス参照、スライシング、基本的な算術演算、そしてブロードキャストやユニバーサル関数といった強力な機能について学びましたね。これで、配列の個々の要素にアクセスしたり、まとめて計算したりする基礎が固まりました。

シリーズ第3回の今回は、NumPy配列をさらに柔軟に、そして効率的に扱うための重要なテクニックを深掘りします!具体的には、

  • 配列の形を思い通りに変える形状変更 (reshape, flatten, ravel)
  • 複数の配列を一つに合体させる配列の結合 (concatenate, vstack, hstack)
  • 一つの配列を複数に分割する配列の分割 (split, vsplit, hsplit)
  • そして、条件に合う要素だけを賢く選び出すブールインデックス参照による条件抽出

といった、データの前処理や分析の現場で頻繁に使われる操作を、具体的なサンプルコードと共に徹底解説します。これらのテクニックを身につければ、あなたのNumPyスキルは確実に次のレベルに進みますよ!


1. 配列の形状変更:データを望みの形にトランスフォーム!

手元にあるNumPy配列の次元数や各次元の要素数を変更したい場面はよくあります。例えば、1次元のデータを2次元の表形式にしたり、機械学習アルゴリズムが要求する特定の形状にデータを合わせたりする場合などです。

1.1. reshape(shape):配列の形状を自由に変更

reshape()メソッド(またはnp.reshape(arr, newshape)関数)は、元の配列の全要素数を変えずに、指定した新しい形状 (shape) に配列を変形します。新しい形状はタプルで指定します。

重要な点: reshape()は、可能であれば元の配列のデータを参照するビューを返します。つまり、reshape()で得た配列の要素を変更すると、元の配列も変更される可能性があります(常にビューが返るとは限りません)。

import numpy as np

arr_1d = np.arange(12) # [ 0  1  2  3  4  5  6  7  8  9 10 11]
print(f"元の1次元配列 (arr_1d):\n{arr_1d}")

# 1次元配列を3行4列の2次元配列に変換
arr_2d_3x4 = arr_1d.reshape((3, 4))
print(f"\n3x4の2次元配列 (arr_2d_3x4):\n{arr_2d_3x4}")

# 2次元配列を別の形状の2次元配列に変換 (2行6列)
arr_2d_2x6 = arr_2d_3x4.reshape((2, 6))
print(f"\n2x6の2次元配列 (arr_2d_2x6):\n{arr_2d_2x6}")

# 形状の要素の一つに -1 を指定すると、残りの次元から自動的に計算される
arr_auto_shape = arr_1d.reshape((4, -1)) # 4行で、列数は自動計算 (この場合は3列)
print(f"\n4x自動計算の配列 (arr_auto_shape):\n{arr_auto_shape}")

# reshape()がビューを返すかどうかの確認(変わる場合がある)
arr_2d_3x4[0, 0] = 99
print(f"\n変更後のarr_2d_3x4[0,0]: {arr_2d_3x4[0,0]}")
print(f"元のarr_1dも影響を受けるか確認: {arr_1d[0]}") # 99 に変わっていることが多い

# 注意: 元の配列の要素数と、新しい形状の要素数が一致しない場合はエラーになります。
# arr_error = arr_1d.reshape((5, 2)) # 要素数12を5x2 (要素数10) にはできない -> ValueError

1.2. flatten()ravel():多次元配列を1次元に平坦化

多次元配列を1次元配列に変換(平坦化)したい場合、flatten()メソッドとravel()メソッド(またはnp.ravel(arr)関数)が使えます。

  • ndarray.flatten(order='C'): 配列を1次元に平坦化した新しい配列(コピー)を返します。元の配列を変更しても影響しません。order='C'で行優先(C言語スタイル、デフォルト)、order='F'で列優先(Fortranスタイル)で平坦化します。
  • ndarray.ravel(order='C'): 配列を1次元に平坦化しますが、可能であれば元の配列のデータを参照するビューを返そうとします。ビューが返された場合、その要素を変更すると元の配列も影響を受けます。コピーが返される場合もあります。
import numpy as np

arr_original = np.array([[1, 2, 3], [4, 5, 6]])
print(f"\n元の2次元配列 (arr_original):\n{arr_original}")

# flatten() の使用 (コピーを返す)
arr_flat_copy = arr_original.flatten()
print(f"flatten()の結果 (arr_flat_copy): {arr_flat_copy}")
arr_flat_copy[0] = 100
print(f"変更後のarr_flat_copy: {arr_flat_copy}")
print(f"元のarr_original (flattenの影響なし): {arr_original[0,0]}") # 1 のまま

# ravel() の使用 (ビューを返すことが多い)
arr_ravel_view = arr_original.ravel()
print(f"\nravel()の結果 (arr_ravel_view): {arr_ravel_view}")
arr_ravel_view[0] = 200 # arr_original[0,0]も200になることが多い
print(f"変更後のarr_ravel_view: {arr_ravel_view}")
print(f"元のarr_original (ravelの影響あり): {arr_original[0,0]}") # 200 に変わっていることが多い

どちらを使うかは、元の配列を変更したくないか(flatten)、メモリ効率を重視するか(ravelでビューが返る場合)によって選択します。


2. 配列の結合:複数のデータを一つにまとめる

複数のNumPy配列を、行方向または列方向に連結して一つの大きな配列にしたい場合があります。これにはnp.concatenate()np.vstack()np.hstack()といった関数が便利です。

2.1. np.concatenate((arr1, arr2, ...), axis=0):指定した軸に沿って結合

np.concatenate()は、タプルで渡された複数の配列を、指定したaxis(軸)に沿って連結します。

  • axis=0: 行方向(縦)に連結します。列数は一致している必要があります。
  • axis=1: 列方向(横)に連結します。行数は一致している必要があります。
import numpy as np

arr_c1 = np.array([[1, 2], [3, 4]])
arr_c2 = np.array([[5, 6]]) # (1,2)の形状。arr_c1と列数は同じ
arr_c3 = np.array([[10], [20]]) # (2,1)の形状。arr_c1と行数は同じ

print(f"\n--- np.concatenate ---")
print(f"arr_c1:\n{arr_c1}")
print(f"arr_c2 (1行2列):\n{arr_c2}")
print(f"arr_c3 (2行1列):\n{arr_c3}")

# 行方向に結合 (axis=0)
concat_axis0 = np.concatenate((arr_c1, arr_c2), axis=0)
print(f"\naxis=0での結合 (arr_c1 と arr_c2):\n{concat_axis0}")

# 列方向に結合 (axis=1)
concat_axis1 = np.concatenate((arr_c1, arr_c3), axis=1)
print(f"\naxis=1での結合 (arr_c1 と arr_c3):\n{concat_axis1}")

# 1次元配列同士の結合
arr_1d_a = np.array([1, 2, 3])
arr_1d_b = np.array([4, 5, 6])
concat_1d = np.concatenate((arr_1d_a, arr_1d_b)) # 1次元の場合、axis=0がデフォルトで、単純な連結
print(f"\n1次元配列の結合:\n{concat_1d}")

2.2. np.vstack(tuple_of_arrays):垂直方向(行方向)にスタック

np.vstack() (vertical stack) は、複数の配列を垂直方向(行方向、axis=0に沿って)に積み重ねて結合します。np.concatenate(..., axis=0)のショートカットと考えることができます。各配列の列数は一致している必要があります。

import numpy as np

arr_v1 = np.array([1, 2, 3]) # (3,)
arr_v2 = np.array([4, 5, 6]) # (3,)
arr_v3 = np.array([[7, 8, 9],[10,11,12]]) # (2,3)


print(f"\n--- np.vstack ---")
# 1次元配列同士をvstackすると、それぞれが行ベクトルとして扱われ、2次元配列になる
stacked_v_1d = np.vstack((arr_v1, arr_v2))
print(f"1次元配列同士のvstack:\n{stacked_v_1d}")
print(f"形状: {stacked_v_1d.shape}") # (2, 3)

# 2次元配列と1次元配列(適切に解釈される)または2次元配列をvstack
stacked_v_mix = np.vstack((arr_v3, arr_v1)) # arr_v1は(1,3)として扱われる
print(f"\n2次元配列と1次元配列のvstack:\n{stacked_v_mix}")

2.3. np.hstack(tuple_of_arrays):水平方向(列方向)にスタック

np.hstack() (horizontal stack) は、複数の配列を水平方向(列方向、axis=1に沿って)に並べて結合します。np.concatenate(..., axis=1)のショートカットと考えることができます。各配列の行数は一致している必要があります。

import numpy as np

arr_h1 = np.array([[1], [2], [3]]) # (3,1)
arr_h2 = np.array([[4], [5], [6]]) # (3,1)
arr_h3 = np.array([[10,11],[20,21],[30,31]]) # (3,2)

print(f"\n--- np.hstack ---")
# 2次元配列(列ベクトル)同士をhstack
stacked_h_2d = np.hstack((arr_h1, arr_h2))
print(f"列ベクトル同士のhstack:\n{stacked_h_2d}")
print(f"形状: {stacked_h_2d.shape}") # (3, 2)

# 2次元配列同士をhstack
stacked_h_mix = np.hstack((arr_h1, arr_h3))
print(f"\n2次元配列同士のhstack:\n{stacked_h_mix}")

# 1次元配列同士をhstackすると、単純に連結される (concatenateと同じ)
arr_1d_c = np.array([1,2])
arr_1d_d = np.array([3,4])
stacked_h_1d = np.hstack((arr_1d_c, arr_1d_d))
print(f"\n1次元配列同士のhstack:\n{stacked_h_1d}") # [1 2 3 4]

vstackhstackは、特に2次元配列を扱う際に行や列を直感的に追加するのに便利です。


3. 配列の分割:一つのデータを複数に分ける

大きな配列を、特定の条件や位置で複数の小さな配列に分割したい場合もあります。これにはnp.split()np.vsplit()np.hsplit()といった関数が使えます。

3.1. np.split(arr, indices_or_sections, axis=0):指定した軸に沿って分割

np.split()は、配列arrを、indices_or_sectionsで指定された方法で、axis(軸)に沿って分割します。

  • indices_or_sectionsが整数の場合: 配列をその数だけ均等に分割しようとします。均等に分割できない場合はエラーになります。
  • indices_or_sectionsが1次元配列(リストやタプル)の場合: その配列の各要素を「分割点」のインデックスとして、そこで配列を分割します。
import numpy as np

arr_to_split = np.arange(12).reshape((3, 4))
print(f"\n分割対象の配列 (arr_to_split):\n{arr_to_split}")

# 行方向 (axis=0) に3つに均等分割
split_rows_equal = np.split(arr_to_split, 3, axis=0)
print(f"\naxis=0で3つに均等分割:")
for i, sub_arr in enumerate(split_rows_equal):
    print(f"分割{i+1}:\n{sub_arr}")

# 列方向 (axis=1) に2つに均等分割
split_cols_equal = np.split(arr_to_split, 2, axis=1)
print(f"\naxis=1で2つに均等分割:")
for i, sub_arr in enumerate(split_cols_equal):
    print(f"分割{i+1}:\n{sub_arr}")

# 列方向 (axis=1) に、1列目と2列目の後で分割 (分割点は [1, 3] -> 0:1, 1:3, 3:末尾)
# indices_or_sections = [1, 3] は、インデックス1の前、インデックス3の前で分割するという意味。
# つまり、[0], [1,2], [3] のように列が分割される
split_cols_points = np.split(arr_to_split, [1, 3], axis=1)
print(f"\naxis=1でインデックス[1,3]を分割点として分割:")
for i, sub_arr in enumerate(split_cols_points):
    print(f"分割{i+1}:\n{sub_arr}")

3.2. np.vsplit(arr, indices_or_sections):垂直方向(行方向)に分割

np.vsplit() (vertical split) は、配列を垂直方向(行方向、axis=0に沿って)に分割します。np.split(arr, indices_or_sections, axis=0)のショートカットです。

import numpy as np
arr_to_vsplit = np.arange(16).reshape((4, 4))
print(f"\n垂直分割対象の配列 (arr_to_vsplit):\n{arr_to_vsplit}")

# 2つの配列に均等に垂直分割
vsplitted_arr = np.vsplit(arr_to_vsplit, 2)
print(f"\n2つに垂直分割:")
print(f"分割1:\n{vsplitted_arr[0]}")
print(f"分割2:\n{vsplitted_arr[1]}")

3.3. np.hsplit(arr, indices_or_sections):水平方向(列方向)に分割

np.hsplit() (horizontal split) は、配列を水平方向(列方向、axis=1に沿って)に分割します。np.split(arr, indices_or_sections, axis=1)のショートカットです。

import numpy as np
arr_to_hsplit = np.arange(16).reshape((4, 4))
print(f"\n水平分割対象の配列 (arr_to_hsplit):\n{arr_to_hsplit}")

# 4つの配列に均等に水平分割
hsplitted_arr = np.hsplit(arr_to_hsplit, 4)
print(f"\n4つに水平分割:")
for i, sub_arr in enumerate(hsplitted_arr):
    print(f"分割{i+1}:\n{sub_arr}")

配列の分割は、例えばデータセットを訓練用とテスト用に分けたり、大きなデータを並列処理のために分割したりする際に役立ちます。


4. 条件による要素の抽出:ブールインデックス参照

前回の記事でも少し触れましたが、NumPy配列に対して比較演算を行うと、結果としてブール値(True/False)の配列が得られます。このブール配列を元の配列のインデックスとして使うと、Trueに対応する位置の要素だけを効率的に抽出できます。これをブールインデックス参照と呼びます。

import numpy as np

arr_cond = np.array([10, 25, 8, 40, 15, 33])
print(f"\n条件抽出対象の配列 (arr_cond): {arr_cond}")

# 条件を作成 (20より大きい要素)
condition = arr_cond > 20
print(f"条件 (arr_cond > 20): {condition}") # [False  True False  True False  True]

# ブール配列を使って要素を抽出
extracted_elements = arr_cond[condition]
print(f"20より大きい要素: {extracted_elements}") # [25 40 33]

# 条件を直接インデックスに記述することも可能
print(f"15以下の要素: {arr_cond[arr_cond <= 15]}") # [10  8 15]

# 複数の条件を組み合わせる (論理積: &, 論理和: |)
# 10より大きく、かつ30未満の要素
multi_condition = (arr_cond > 10) & (arr_cond < 30)
print(f"\n10より大きく30未満の要素: {arr_cond[multi_condition]}") # [25 15]

# 2次元配列への適用
arr2d_cond = np.array([[1, 5, 10], [15, 2, 8]])
print(f"\n2次元配列 arr2d_cond:\n{arr2d_cond}")
print(f"arr2d_cond > 5:\n{arr2d_cond[arr2d_cond > 5]}") # 条件を満たす要素が1次元配列で返る [10 15  8]

# 条件に合致した要素に値を代入することも可能
arr_assign = np.arange(10)
print(f"\n代入前のarr_assign: {arr_assign}")
arr_assign[arr_assign % 2 == 0] = 99 # 偶数の要素を99に置き換え
print(f"偶数を99に置き換え後: {arr_assign}")

ブールインデックス参照は、データの中から特定の条件を満たすものだけを選び出したり、一括で値を変更したりする際に非常に強力なテクニックです。


まとめ:NumPy配列操作の応用力を高めよう!

今回は、NumPyシリーズの第3回として、配列のより高度な操作方法について学びました。

  • 配列の形状変更 (reshape(), flatten(), ravel()) で、データを分析しやすい形に変形する方法。
  • 複数の配列を一つにまとめる配列の結合 (np.concatenate(), np.vstack(), np.hstack())。
  • 大きな配列を扱いやすい単位に分ける配列の分割 (np.split(), np.vsplit(), np.hsplit())。
  • 特定の条件を満たす要素だけを効率的に選び出すブールインデックス参照による条件抽出

これらのテクニックをマスターすることで、NumPyを使ったデータの前処理や操作が格段にスムーズになり、より複雑な分析にも対応できるようになります。特にブールインデックス参照は、データ分析の現場では頻繁に使われる必須のスキルと言えるでしょう。

NumPyの配列操作は奥が深く、ここで紹介したのはまだ一部です。しかし、これらの基本をしっかり押さえておけば、より高度なテクニックも理解しやすくなるはずです。次回は、NumPyを使ったファイルへのデータの保存・読み込みや、簡単な線形代数演算など、さらに実践的な内容に触れていきたいと思います。お楽しみに!

【NumPy中級編1 第4回】データ永続化と計算の基礎!ファイル入出力&線形代数の初歩 (save/load, dot, @, T)

その他の投稿はこちら

コメント

このブログの人気の投稿

タイトルまとめ

これで迷わない!WindowsでPython環境構築する一番やさしい方法 #0

【Python標準ライブラリ完結!】11の冒険をありがとう!君のPython力が飛躍する「次の一歩」とは? 🚀