メインコンテンツへスキップ

3.2.4 配列演算

NumPy のブロードキャストとベクトル化演算の図

学習目標

  • ベクトル化演算の概念と利点を理解する
  • 要素ごとの演算と汎用関数(ufunc)を身につける
  • ブロードキャスト機構(Broadcasting)のルールを理解する
  • 集約関数を使って統計計算を行えるようにする

ベクトル化演算:ループからの卒業

ベクトル化演算は NumPy の核心的な考え方です。つまり、配列全体に対して操作し、ループを書かずに処理します。

純 Python vs NumPy

import numpy as np

# 純 Python:1つずつ計算
prices = [100, 200, 300, 400, 500]
discounted = []
for p in prices:
discounted.append(p * 0.8)
print(discounted) # [80.0, 160.0, 240.0, 320.0, 400.0]

# NumPy:1行で完了
prices = np.array([100, 200, 300, 400, 500])
discounted = prices * 0.8
print(discounted) # [ 80. 160. 240. 320. 400.]

要素ごとの演算

NumPy 配列の算術演算は要素ごとに行われます。

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

print(a + b) # [11 22 33 44] 対応する位置どうしを足す
print(a - b) # [ -9 -18 -27 -36]
print(a * b) # [ 10 40 90 160] 対応する位置どうしを掛ける(行列積ではありません!)
print(a / b) # [0.1 0.1 0.1 0.1]
print(a ** 2) # [ 1 4 9 16] 2乗
print(b % 3) # [1 2 0 1] 余り
print(b // 3) # [ 3 6 10 13] 切り捨て除算

スカラーとの演算

配列と 1 つの数値(スカラー)を計算するときは、スカラーが自動的に各要素へ適用されます。

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

print(arr + 5) # [15 25 35 45]
print(arr * 2) # [20 40 60 80]
print(arr / 10) # [1. 2. 3. 4.]
print(1 / arr) # [0.1 0.05 0.033 0.025]

比較演算

arr = np.array([15, 23, 8, 42, 31])

print(arr > 20) # [False True False True True]
print(arr == 23) # [False True False False False]
print(arr != 8) # [ True True False True True]

汎用関数(ufunc)

NumPy にはたくさんの汎用関数があり、配列の各要素に数学的な演算を適用できます。

よく使う数学関数

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

# 平方根
print(np.sqrt(arr)) # [1. 2. 3. 4. 5.]

# 絶対値
neg = np.array([-3, -1, 0, 2, 5])
print(np.abs(neg)) # [3 1 0 2 5]

# べき乗
print(np.power(arr, 0.5)) # sqrt と同じ

# 指数と対数
print(np.exp([0, 1, 2])) # [1. 2.718 7.389] e のべき乗
print(np.log([1, np.e, 10])) # [0. 1. 2.303] 自然対数
print(np.log10([1, 10, 100])) # [0. 1. 2.] 10 を底とする
print(np.log2([1, 2, 8, 64])) # [0. 1. 3. 6.] 2 を底とする

三角関数

# 0 から 2π までの角度を作成
angles = np.linspace(0, 2 * np.pi, 5) # [0, π/2, π, 3π/2, 2π]

print(np.sin(angles)) # [ 0. 1. 0. -1. 0.] ← 正弦
print(np.cos(angles)) # [ 1. 0. -1. 0. 1.] ← 余弦

切り上げ・切り捨て関数

arr = np.array([1.2, 2.5, 3.7, -1.3, -2.8])

print(np.floor(arr)) # [ 1. 2. 3. -2. -3.] 切り捨て
print(np.ceil(arr)) # [ 2. 3. 4. -1. -2.] 切り上げ
print(np.round(arr)) # [ 1. 2. 4. -1. -3.] 四捨五入
print(np.trunc(arr)) # [ 1. 2. 3. -1. -2.] 小数点以下を切り捨て

2 つの配列間の演算

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

print(np.maximum(a, b)) # [3 5 7 9] 対応する位置で大きい方を取る
print(np.minimum(a, b)) # [1 4 2 8] 対応する位置で小さい方を取る
print(np.where(a > b, a, b)) # maximum と同じだが、より柔軟

ブロードキャスト機構(Broadcasting)

形状の異なる 2 つの配列で演算するとき、NumPy は自動的に小さい配列を「ブロードキャスト」して、形状をそろえます。

いちばん簡単な例

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

# スカラー + 配列 → スカラーが [10, 10, 10] にブロードキャストされる
print(arr + 10) # [11 12 13]

これはブロードキャストの例です。NumPy は 10[10, 10, 10] に広げてから、要素ごとに足しています。

2 次元配列 + 1 次元配列

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

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

# row が各行にブロードキャストされる
result = matrix + row
print(result)
# [[11 22 33]
# [14 25 36]
# [17 28 39]]

ブロードキャストの流れは次のように理解できます。

matrix:         row (ブロードキャスト前):  row (ブロードキャスト後):
[[1, 2, 3], [10, 20, 30] → [[10, 20, 30],
[4, 5, 6], [10, 20, 30],
[7, 8, 9]] [10, 20, 30]]

列ベクトル + 行ベクトル

col = np.array([[1], [2], [3]])    # shape: (3, 1) 列ベクトル
row = np.array([10, 20, 30]) # shape: (3,) 行ベクトル

# 両方がブロードキャストされる
result = col + row
print(result)
# [[11 21 31]
# [12 22 32]
# [13 23 33]]

ブロードキャストのルール

覚え方は簡単です。次元は後ろから比べて、同じか、どちらかが 1 なら OK です。

# ✅ ブロードキャストできる
# (3, 4) + (4,) → (3, 4) 最後の次元はどちらも 4
# (3, 4) + (1, 4) → (3, 4) 1 行目の 3 と 1 → 3 に広がる
# (3, 1) + (1, 4) → (3, 4) 両方の次元がブロードキャストされる

# ❌ ブロードキャストできない
# (3, 4) + (3,) → エラー!最後の次元は 4 ≠ 3 で、どちらも 1 ではない

ブロードキャストの実用例

# データの標準化:各列からその列の平均を引く
data = np.array([
[85, 170, 60],
[92, 180, 75],
[78, 165, 55],
[90, 175, 70]
]) # 4 人の学生:成績、身長、体重

# 各列の平均を計算
col_mean = data.mean(axis=0) # [86.25 172.5 65. ] shape: (3,)

# ブロードキャスト:(4, 3) - (3,) → (4, 3)
centered = data - col_mean
print(centered)
# [[-1.25 -2.5 -5. ]
# [ 5.75 7.5 10. ]
# [-8.25 -7.5 -10. ]
# [ 3.75 2.5 5. ]]

集約関数

集約関数は、複数のデータを 1 つ、または少数の値に「まとめる」関数です。

よく使う集約関数

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

print(np.sum(arr)) # 45 合計
print(np.mean(arr)) # 5.0 平均
print(np.median(arr)) # 5.0 中央値
print(np.std(arr)) # 2.58 標準偏差
print(np.var(arr)) # 6.67 分散
print(np.min(arr)) # 1 最小値
print(np.max(arr)) # 9 最大値
print(np.argmin(arr)) # 4 最小値のインデックス
print(np.argmax(arr)) # 3 最大値のインデックス
print(np.cumsum(arr)) # [ 4 11 13 22 23 28 36 39 45] 累積和
print(np.cumprod(arr[:5])) # [ 4 28 56 504 504] 累積積

axis を使った集約

多次元配列では、axis パラメータでどの方向に沿って集約するかを指定します。

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

# axis を指定しない:すべての要素をまとめる
print(np.sum(matrix)) # 45

# axis=0:行方向に沿って(列ごとに)集約する —— 上下にまとめる
print(np.sum(matrix, axis=0)) # [12 15 18]

# axis=1:列方向に沿って(行ごとに)集約する —— 左右にまとめる
print(np.sum(matrix, axis=1)) # [ 6 15 24]

axis の考え方は、axis=0 は行を消す、axis=1 は列を消す です。

元の形 (3, 3):
[[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]

axis=0 (行を消す → 結果の shape=(3,)):
[1+4+7, 2+5+8, 3+6+9] = [12, 15, 18]

axis=1 (列を消す → 結果の shape=(3,)):
[1+2+3, 4+5+6, 7+8+9] = [6, 15, 24]

実践:成績分析

# 5 人の学生の 3 科目の成績
scores = np.array([
[85, 92, 78], # 学生1:国語、数学、英語
[90, 88, 95], # 学生2
[72, 65, 80], # 学生3
[95, 98, 92], # 学生4
[60, 55, 70] # 学生5
])

subjects = ["国語", "数学", "英語"]

# 各学生の合計点
total = np.sum(scores, axis=1)
print("各学生の合計点:", total) # [255 273 217 285 185]

# 各学生の平均点
avg_per_student = np.mean(scores, axis=1)
print("各学生の平均点:", avg_per_student)

# 各科目の平均点
avg_per_subject = np.mean(scores, axis=0)
for sub, avg in zip(subjects, avg_per_subject):
print(f" {sub}平均点: {avg:.1f}")

# クラス全体で最高点は誰のどの科目か
max_idx = np.unravel_index(np.argmax(scores), scores.shape)
print(f"最高点: {scores[max_idx]} (学生{max_idx[0]+1}{subjects[max_idx[1]]})")

# 合計点がいちばん高い学生
best_student = np.argmax(total)
print(f"合計点が最高: 学生{best_student + 1}, 合計点 {total[best_student]}")

np.where:条件による選択

np.where は、NumPy 版の三項演算子です。

arr = np.array([85, 42, 91, 67, 55, 78])

# 合格は "PASS"、不合格は "FAIL" とする
result = np.where(arr >= 60, "PASS", "FAIL")
print(result) # ['PASS' 'FAIL' 'PASS' 'PASS' 'FAIL' 'PASS']

# 不合格の点数を 60 にする
adjusted = np.where(arr >= 60, arr, 60)
print(adjusted) # [85 60 91 67 60 78]

まとめ

カテゴリ内容
ベクトル化演算配列全体をまとめて計算し、ループ不要arr * 2, a + b
汎用関数要素ごとの数学関数np.sqrt(), np.exp(), np.log()
ブロードキャスト形状の異なる配列を自動で拡張する(3,4) + (4,)(3,4)
集約関数統計量をまとめて計算するnp.sum(), np.mean(), np.std()
axis パラメータ集約する方向を指定するaxis=0 は列ごと、axis=1 は行ごと
np.where条件による選択np.where(arr > 0, arr, 0)

手を動かしてみよう

練習 1:ベクトル化計算

# 華氏温度を摂氏温度に変換する
# 公式:C = (F - 32) × 5/9
import numpy as np

fahrenheit = np.array([32, 68, 100, 212, 72, 98.6])

# ベクトル化演算で 1 行で変換する
celsius = (fahrenheit - 32) * 5 / 9

練習 2:ブロードキャストの練習

# 3 つの商品の元の価格
import numpy as np

prices = np.array([100, 200, 300])

# 3 種類の割引率(列ベクトル)
discounts = np.array([[0.9], [0.8], [0.7]])

# ブロードキャストを使って、各商品が各割引率のときの価格を計算する(3×3 行列)
final_prices = discounts * prices
# 期待結果:
# [[ 90. 180. 270.]
# [ 80. 160. 240.]
# [ 70. 140. 210.]]

練習 3:成績統計

# 50 人の学生のランダムな成績を生成する(40〜100 の範囲)
rng = np.random.default_rng(seed=42)
scores = rng.integers(40, 101, size=50)

# 1. 平均、中央値、標準偏差を計算する
# 2. 最高点、最低点とその位置を見つける
# 3. 各点数帯の人数を集計する:不合格(<60)、合格(60-69)、普通(70-79)、良い(80-89)、優秀(90+)
# 4. 合格率を計算する