【NumPy入門 第2回】配列操作の基礎!インデックス参照・スライシング・算術演算・ブロードキャスト・便利な数学関数 (ufunc) 徹底解説
「NumPyで配列を作ったはいいけど、特定の部分だけ取り出したり、まとめて計算したりするにはどうすればいいの?」
「Pythonのリストとは違う、NumPyならではの便利な配列操作って何だろう?」
「ブロードキャストとかユニバーサル関数とか、よく聞くけど難しそう…」
こんにちは! NumPy探検隊、隊長のPythonistaです! 前回の第1回では、NumPyの重要性、インストール方法、そして中心的なデータ構造であるndarray
の様々な作成方法と基本的な属性について学びましたね。これで、数値データを効率的に扱うための強力な「箱」を手に入れたことになります。
シリーズ第2回の今回は、いよいよその「箱」の中身を自由自在に操るための基本操作を徹底解説します! 具体的には、
- 配列の特定の要素や部分を取り出すインデックス参照とスライシング
- 配列同士や配列と数値の算術演算(要素ごとの計算)
- 形状の異なる配列同士でも賢く計算できるNumPyの魔法、ブロードキャストの基本
- 配列全体の各要素に一括で適用できる便利な数学関数(ユニバーサル関数 - ufunc)
などを、1次元配列と2次元配列(行列)の例を交えながら分かりやすく説明していきます。これらの基本操作をマスターすれば、NumPyを使ったデータ処理や分析が格段にスムーズになりますよ!
1. NumPy配列へのアクセス:インデックス参照とスライシング
NumPy配列の要素にアクセスしたり、一部分を切り出したりする方法は、Pythonのリストと似ている部分も多いですが、多次元配列ならではの便利な指定方法もあります。
1.1. 1次元配列のインデックス参照とスライシング
1次元配列の場合、Pythonのリストとほぼ同じ感覚でインデックス参照やスライシングを行えます。
import numpy as np
arr1d = np.array([10, 20, 30, 40, 50, 60])
print(f"1次元配列 arr1d:\n{arr1d}")
# インデックス参照 (0から始まる)
print(f"\narr1d[0]: {arr1d[0]}") # 先頭の要素: 10
print(f"arr1d[2]: {arr1d[2]}") # 3番目の要素: 30
print(f"arr1d[-1]: {arr1d[-1]}") # 末尾の要素: 60
# スライシング (start:stop:step) stopは含まない
print(f"\narr1d[1:4]: {arr1d[1:4]}") # インデックス1から3まで: [20 30 40]
print(f"arr1d[:3]: {arr1d[:3]}") # 先頭からインデックス2まで: [10 20 30]
print(f"arr1d[3:]: {arr1d[3:]}") # インデックス3から末尾まで: [40 50 60]
print(f"arr1d[::2]: {arr1d[::2]}") # 全体を2つ飛ばしで: [10 30 50]
1.2. 2次元配列(行列)のインデックス参照とスライシング
2次元配列では、[行インデックス, 列インデックス]
のようにカンマで区切って指定するのが一般的です。[行インデックス][列インデックス]
という書き方も可能ですが、前者が推奨されます。
import numpy as np
arr2d = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"2次元配列 arr2d:\n{arr2d}")
# 特定の要素にアクセス (行, 列)
print(f"\narr2d[0, 0]: {arr2d[0, 0]}") # 0行0列目の要素: 1
print(f"arr2d[1, 2]: {arr2d[1, 2]}") # 1行2列目の要素: 7
print(f"arr2d[2, -1]: {arr2d[2, -1]}") # 2行目の最後の要素: 12
# 特定の行全体を取得 (スライス)
print(f"\narr2d[0, :]: {arr2d[0, :]}") # 0行目全体: [1 2 3 4]
print(f"arr2d[1]: {arr2d[1]}") # これも0行目全体と同じ結果になる
# 特定の列全体を取得 (スライス)
print(f"\narr2d[:, 1]: {arr2d[:, 1]}") # 1列目全体: [ 2 6 10]
# 部分行列を取得 (スライス)
# 0行目から1行目まで (2行目は含まない)、1列目から2列目まで (3列目は含まない)
sub_array = arr2d[0:2, 1:3]
print(f"\narr2d[0:2, 1:3]:\n{sub_array}")
# 結果:
# [[2 3]
# [6 7]]
# 最後の2行、最初の2列
sub_array2 = arr2d[-2:, :2]
print(f"\narr2d[-2:, :2]:\n{sub_array2}")
2次元配列のスライシングは、データの一部分を効率的に取り出すのに非常に強力です。慣れるまでは、図でどの部分が切り出されるかイメージしながら試してみると良いでしょう。
1.3. スライスはビュー(参照):元の配列も変更される!
Pythonのリストのスライスは元のリストのコピーを作成しますが、NumPyの配列のスライスは、多くの場合、元の配列のデータを参照する「ビュー (view)」を返します。これは、大きなデータを扱う際に不要なコピーを防ぎ、メモリ効率と速度を向上させるためです。
重要な注意点: スライス(ビュー)に対して変更を加えると、元の配列のデータも変更されてしまいます。
import numpy as np
original_arr = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(f"\n元の配列 original_arr: {original_arr}")
slice_arr = original_arr[3:6] # [3 4 5]
print(f"スライス slice_arr: {slice_arr}")
# スライスした配列の要素を変更
slice_arr[0] = 999
print(f"変更後のslice_arr: {slice_arr}") # [999 4 5]
print(f"変更後のoriginal_arr: {original_arr}") # 元の配列も変わっている! [ 0 1 2 999 4 5 6 7 8 9]
# もしスライスを独立したコピーとして扱いたい場合は、.copy()メソッドを使う
copied_slice_arr = original_arr[6:8].copy()
print(f"\nコピーしたスライス copied_slice_arr: {copied_slice_arr}")
copied_slice_arr[0] = 777
print(f"変更後のcopied_slice_arr: {copied_slice_arr}")
print(f"変更後もoriginal_arrは変わらない: {original_arr}")
この「ビュー」の挙動は、最初は戸惑うかもしれませんが、NumPyのパフォーマンスにとって重要な特性です。意図しない変更を避けるためには、.copy()
を適切に使いましょう。
2. NumPy配列の算術演算:要素ごと(要素ワイズ)の計算が基本!
NumPy配列の大きな利点の一つは、ループを使わずに配列全体に対して高速に算術演算を行えることです。これらの演算は、基本的に**要素ごと (element-wise)** に実行されます。
2.1. 配列とスカラ値(単一の数値)の演算
配列とスカラ値の演算では、配列の各要素に対してそのスカラ値との演算が行われます。
import numpy as np
arr = np.array([1, 2, 3, 4])
print(f"\n配列 arr: {arr}")
print(f"arr + 5: {arr + 5}") # 各要素に5を足す: [ 6 7 8 9]
print(f"arr - 2: {arr - 2}") # 各要素から2を引く: [-1 0 1 2]
print(f"arr * 10: {arr * 10}") # 各要素を10倍: [10 20 30 40]
print(f"arr / 2: {arr / 2}") # 各要素を2で割る: [0.5 1. 1.5 2. ] (結果はfloat)
print(f"arr // 2: {arr // 2}") # 各要素を2で割った商(整数): [0 1 1 2]
print(f"arr ** 2: {arr ** 2}") # 各要素を2乗: [ 1 4 9 16]
2.2. 同じ形状の配列同士の演算
同じ形状(shape)を持つ配列同士の算術演算も、対応する要素ごとに行われます。
import numpy as np
arr_a = np.array([[1, 2], [3, 4]])
arr_b = np.array([[10, 20], [30, 40]])
print(f"\n配列 arr_a:\n{arr_a}")
print(f"配列 arr_b:\n{arr_b}")
print(f"\narr_a + arr_b (要素ごとの和):\n{arr_a + arr_b}")
print(f"arr_a * arr_b (要素ごとの積):\n{arr_a * arr_b}")
# Pythonのリストの + 演算子(連結)とは異なることに注意!
list_a = [1, 2]
list_b = [10, 20]
print(f"\nPythonリストの + : {list_a + list_b}") # [1, 2, 10, 20]
比較演算(>
, <
, ==
など)も要素ごとに行われ、結果はブール値(True
/False
)の配列になります。
import numpy as np
arr = np.array([10, 15, 20, 25])
print(f"\n配列 arr: {arr}")
print(f"arr > 18: {arr > 18}") # [False False True True]
このブール配列は、後述する条件による要素の抽出(ブールインデックス参照)で非常に役立ちます。
3. NumPyの魔法!ブロードキャストの基本を理解しよう
NumPyの非常に強力で便利な機能の一つに「ブロードキャスト (Broadcasting)」があります。これは、形状が異なる配列同士でも、NumPyが(一定のルールに従って)自動的に配列の形状を仮想的に「拡張」して、要素ごとの演算を可能にする仕組みです。
ループ処理を明示的に書く必要がなくなり、コードが簡潔かつ効率的になります。
3.1. ブロードキャストとは?
一番簡単なブロードキャストの例は、既に出てきた「配列とスカラ値の演算」です。スカラ値が配列の各要素に対して「ブロードキャスト」されて演算が行われています。
import numpy as np
arr = np.array([1, 2, 3])
scalar = 100
result = arr + scalar # scalarが [100, 100, 100] のように拡張されて計算されるイメージ
print(f"\nブロードキャスト (配列 + スカラ): {result}") # [101 102 103]
3.2. ブロードキャストの基本的なルール(簡単な紹介)
ブロードキャストが成立するためには、いくつかのルールがあります。ここでは非常に簡単に説明します(詳細は公式ドキュメントを参照してください)。
- 2つの配列の次元数が異なる場合、次元数の少ない方の配列の形状の先頭に1を追加して次元数を合わせます。
- 各次元で、2つの配列の要素数が同じであるか、どちらか一方の要素数が1である場合、ブロードキャスト可能です。要素数が1の方は、もう一方の配列の要素数に合わせて「引き伸ばされる」ように振る舞います。
- どちらの条件も満たさない次元がある場合は、エラーになります。
3.3. 簡単なブロードキャストの例
例1:2次元配列(行列)と1次元配列(ベクトル)の演算
import numpy as np
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]) # (3, 3) 配列
vector_row = np.array([10, 20, 30]) # (3,) 配列 (1次元)
# vector_rowが各行に対してブロードキャストされる
result1 = matrix + vector_row
print(f"\n行列 + 行ベクトル (ブロードキャスト):\n{result1}")
# 結果:
# [[11 22 33]
# [14 25 36]
# [17 28 39]]
vector_col = np.array([[100], [200], [300]]) # (3, 1) 配列 (列ベクトル)
# vector_colが各列に対してブロードキャストされる
result2 = matrix + vector_col
print(f"\n行列 + 列ベクトル (ブロードキャスト):\n{result2}")
# 結果:
# [[101 102 103]
# [204 205 206]
# [307 308 309]]
ブロードキャストは、最初は少し直感に反するように感じるかもしれませんが、慣れると非常に強力で、多くのループ処理を不要にしてくれます。
4. NumPyの便利な数学関数(ユニバーサル関数 - ufunc)
NumPyには、配列の各要素に対して高速に数学的な演算を行うための「ユニバーサル関数 (Universal Functions、略してufunc)」が豊富に用意されています。これらも要素ごとの演算であり、ループを書く必要がありません。
4.1. ユニバーサル関数 (ufunc) とは?
ufuncは、NumPyのndarray
オブジェクトを引数に取り、新しいndarray
を返す関数です。算術演算子(+
, *
など)も実はufuncの一種として実装されています。
4.2. 代表的なufuncの紹介
集計系の関数:
np.sum(arr, axis=None)
: 配列arr
の全要素の合計、または指定した軸(axis
)に沿った合計を計算します。axis=0
で行方向(各列の合計)、axis=1
で列方向(各行の合計)の集計ができます(2次元配列の場合)。np.mean(arr, axis=None)
: 平均値。np.max(arr, axis=None)
,np.min(arr, axis=None)
: 最大値、最小値。np.std(arr, axis=None)
,np.var(arr, axis=None)
: 標準偏差、分散。- これらの多くは、
arr.sum()
,arr.mean()
のように、ndarray
オブジェクトのメソッドとしても呼び出せます。
import numpy as np
arr_stats = np.array([[1, 2, 3], [4, 5, 6]])
print(f"\n配列 arr_stats:\n{arr_stats}")
print(f"全要素の合計 (np.sum): {np.sum(arr_stats)}") # 21
print(f"全要素の合計 (メソッド): {arr_stats.sum()}") # 21
print(f"列ごとの合計 (axis=0): {np.sum(arr_stats, axis=0)}") # [5 7 9]
print(f"行ごとの合計 (axis=1): {np.sum(arr_stats, axis=1)}") # [ 6 15]
print(f"全要素の平均: {np.mean(arr_stats)}") # 3.5
print(f"全要素の最大値: {arr_stats.max()}") # 6
要素ごとの数学関数:
np.sqrt(arr)
: 各要素の平方根。np.exp(arr)
: 各要素の指数関数 ($e^x$)。np.log(arr)
,np.log10(arr)
,np.log2(arr)
: 各要素の自然対数、常用対数(底10)、二進対数(底2)。np.sin(arr)
,np.cos(arr)
,np.tan(arr)
: 各要素の三角関数(引数はラジアン単位)。np.abs(arr)
またはnp.absolute(arr)
: 各要素の絶対値。np.round(arr, decimals=0)
: 各要素を指定した小数点以下の桁数で四捨五入。- その他多数! (
np.ceil()
,np.floor()
,np.square()
など)
import numpy as np
arr_math = np.array([1, 4, 9, 16])
print(f"\n配列 arr_math: {arr_math}")
print(f"平方根 (np.sqrt): {np.sqrt(arr_math)}") # [1. 2. 3. 4.]
angles = np.array([0, np.pi/2, np.pi]) # 0, 90度, 180度 (np.piは円周率)
print(f"\n角度 (ラジアン): {angles}")
print(f"サイン (np.sin): {np.sin(angles)}") # [0.0000000e+00 1.0000000e+00 1.2246468e-16] (ほぼ0, 1, 0)
values = np.array([-1.7, 2.3, 0.0, -5.0])
print(f"\n配列 values: {values}")
print(f"絶対値 (np.abs): {np.abs(values)}")
print(f"四捨五入 (np.round): {np.round(values)}")
これらのufuncを使うことで、Pythonのループで書くよりもはるかに高速で簡潔なコードになります。
まとめ:NumPy配列操作の強力な第一歩!
今回は、NumPyシリーズの第2回として、ndarray
の基本的な操作方法について学びました。
- 1次元・2次元配列のインデックス参照と、一部分を効率的に切り出すスライシング。
- スライスが元の配列の「ビュー」であることの注意点と、コピーのための
.copy()
メソッド。 - 配列とスカラ値、または配列同士の算術演算が、要素ごと(要素ワイズ)に行われること。
- 形状の異なる配列間でも演算を可能にするNumPyの魔法、ブロードキャストの基本的な考え方と例。
- 配列の各要素に高速に数学的処理を適用できるユニバーサル関数 (ufunc) の代表例(集計系、要素ごとの数学関数)。
これらの基本操作をマスターすることで、NumPyを使ったデータ処理や数値計算のコードが格段に書きやすくなり、その効率の良さも実感できるはずです。Pythonのリストでは煩雑だった処理も、NumPyを使えば数行で、しかも高速に実行できる場面がたくさんあります。
次回は、NumPy配列の形状を自由自在に変更する方法(reshape
など)や、複数の配列を結合したり分割したりする方法、そして条件に基づいて要素を抽出するさらに進んだテクニックについて詳しく見ていきます。お楽しみに!
コメント
コメントを投稿