ndarrayの基本概念
NumPyの中核となるデータ構造はndarray、つまりN次元配列です。見た目はPythonのリストに似ていますが、その内部構造はまったく異なります。型が固定されており、メモリが連続しており、演算がベクトル化されています。ndarrayの内部構造を理解することは、NumPyを効果的に活用するための第一歩です。
1. 学習内容
- ❶ ndarrayの本質:同質、固定型、連続したメモリ
- ❷ 次元数(ndim)、形状、およびサイズ
- ❸ 軸の背後にある直感
- ❹ dtype システムとメモリ使用量
- ❺ ndarray とリストのメモリモデルの比較
2. データアナリストの実体験
(1) 問題:PythonでのExcel操作、混乱
ボブは、3行4列のExcel表を持っています。彼はこれを、外側のリストの中に3つのサブリストがネストされたPythonのリストのリストとして保存しています。2列目の合計を求めたいので、各行を反復処理するためのforループを書きました。 100万行の場合、これには5秒かかります。さらに悪いことに、行のどこかに文字列が紛れ込んでしまうと、そのセルに到達するまでループは失敗しません。
(2) ndarray による解決策
チャーリーは、ボブに対し、np.array を使ってテーブルを ndarray に変換するよう提案しています。 NumPyはデータを連続したメモリブロックに平坦化し、Cレベルのベクトル化演算で列の合計を一度に実行します。これにより、同じ100万行の処理でもわずか5ミリ秒で完了します。型チェックは生成時に強制されるため、誤って混入した文字列は計算の途中でではなく、即座に検出されます。
(3) 成果
- 計算速度が1000倍向上
- 型の不一致は、生成時に検出されます
- 形状、軸、ストライドなどのメタデータにより、多次元操作が直感的に行える
3. ndarrayの本質
(1) 均質データ型
ndarrayを構成するすべての要素は、同じ型でなければなりません。これはPythonのリストとは根本的に異なります。リストでは整数、文字列、さらにはオブジェクトを混在させることができますが、ndarrayでは1つのdtypeしか使用できません。
import numpy as np
a = np.array([1, 2, 3])
print(a.dtype) # int64
b = np.array([1.0, 2.0, 3.0])
print(b.dtype) # float64
c = np.array([1, 2.0, 3])
print(c.dtype) # float64 (auto upcast)
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(2) 連続メモリ領域
Pythonのリストはポインタの配列を格納します。各ポインタは、ヒープメモリ上に散在する個別のPyObjectを指しています。一方、ndarrayはすべてのデータをメモリ上の単一の連続したブロックに配置するため、ポインタによるオーバーヘッドがなく、CPUキャッシュの効率も非常に高くなります。
| 特集 | Pythonのリスト | ndarray |
|---|---|---|
| 要素の型 | 任意の組み合わせ | 同じデータ型 |
| 格納 | ポインタ配列 → 分散した PyObject | 連続したメモリブロック |
| 要素ごとのオーバーヘッド | 28バイト以上(PyObjectヘッダー) | dtypeに依存(1/2/4/8バイト) |
| キャッシュ効率が良い | 低い(ポインタジャンプ) | 非常に高い(順次アクセス) |
| 型安全性 | なし(実行時に検出される) | 作成時に強制される |
(1) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: 0次元/1次元/2次元/3次元配列の特性 (難易度 ⭐)
import numpy as np
a0 = np.array(42) # 0D scalar
a1 = np.array([1, 2, 3]) # 1D
a2 = np.array([[1, 2], [3, 4]]) # 2D
a3 = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]]) # 3D
for name, arr in [("0D", a0), ("1D", a1), ("2D", a2), ("3D", a3)]:
print(f"{name}: ndim={arr.ndim}, shape={arr.shape}, size={arr.size}")
# 0D: ndim=0, shape=(), size=1
# 1D: ndim=1, shape=(3,), size=3
# 2D: ndim=2, shape=(2,2), size=4
# 3D: ndim=3, shape=(2,2,2), size=8
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
4. 寸法、形状、およびサイズ
(1) 数 / 形状 / 大きさ
| 属性 | 意味 | 例 (shape=(3,4)) |
|---|---|---|
ndim |
次元数(軸数) | 2 |
shape |
各軸の長さ(タプルとして) | (3, 4) |
size |
要素の総数 = 軸の長さの積 | 12 |
import numpy as np
a = np.zeros((3, 4))
print(a.ndim) # 2
print(a.shape) # (3, 4)
print(a.size) # 12
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(2) ストライド
strides は、指定された軸に沿って1ステップ移動するために、メモリ上で何バイトスキップすべきかを示します。
| 属性 | 意味 | shape=(3,4) dtype=int64 |
|---|---|---|
strides |
軸ごとのストライド(バイト) | (32, 8) |
itemsize |
要素あたりのバイト数 | 8 |
import numpy as np
a = np.zeros((3, 4), dtype=np.int64)
print(a.strides) # (32, 8)
# axis 0: move 1 row = 4 * 8 = 32 bytes
# axis 1: move 1 col = 1 * 8 = 8 bytes
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(1) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: ストライドの可視化(難易度 ⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int32).reshape(3, 4)
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(f"shape: {a.shape}") # (3, 4)
print(f"strides: {a.strides}") # (16, 4)
print(f"itemsize: {a.itemsize}") # 4
# axis 0 stride: 4 elements * 4 bytes = 16
# axis 1 stride: 1 element * 4 bytes = 4
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
5. 軸の背後にある直感
軸は、ndarray の操作を理解するための鍵となります。axis=0 は最外層の次元、axis=1 はその次の次元、というように続きます。
| 軸 | 図形内での位置 | 直感 | 2D図形=(3,4) | 3D図形=(2,3,4) |
|---|---|---|---|---|
| 軸=0 | 0番目 | 最外側 | 行方向(行を横切る) | 「層」軸に沿って |
| 軸=1 | 第1 | 第2 | 列方向(列を横切る) | 「行」軸に沿って |
| 軸=2 | 2番目 | 最も内側 | — | 「列」軸に沿って |
覚え方:axis=k は shape[k] の方向に対応しています。その軸に沿って sum/reduce を行うと、その次元が「消去」されます。
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print(a.shape) # (2, 3, 4)
print(a.sum(axis=0).shape) # (3, 4) — axis 0 eliminated
print(a.sum(axis=1).shape) # (2, 4) — axis 1 eliminated
print(a.sum(axis=2).shape) # (2, 3) — axis 2 eliminated
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
6. dtype とメモリ使用量
(1) dtype 型システム
| データ型 | 説明 | バイト数 | 範囲 |
|---|---|---|---|
np.int8 |
符号付き8ビット整数 | 1 | -128 ~ 127 |
np.int32 |
符号付き32ビット整数 | 4 | -2^31 ~ 2^31-1 |
np.int64 |
符号付き64ビット整数 | 8 | -2^63 ~ 2^63-1 |
np.float32 |
32ビット浮動小数点 | 4 | ~±3.4e38、7桁の精度 |
np.float64 |
64ビット浮動小数点 | 8 | ~±1.8e308、15桁の精度 |
np.bool_ |
ブール値 | 1 | 真/偽 |
np.complex128 |
128ビット複素数 | 16 | float64 2個 |
(2) メモリモデル:ndarrayはネストされたリストではない
graph TB
A["ndarray object"] --> B["data pointer<br/>raw memory block"]
A --> C["dtype<br/>element type"]
A --> D["shape<br/>(3, 4)"]
A --> E["strides<br/>(32, 8)"]
B --> F["0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | ... | 2,3"]
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style F fill:#FF9800,color:#fff
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(3) 形状記憶特性参照表
| 形状 | 次元数 | サイズ | データ型=float64 バイト数 | ストライド |
|---|---|---|---|---|
| (10,) | 1 | 10 | 80 | (8,) |
| (3, 4) | 2 | 12 | 96 | (32, 8) |
| (2, 3, 4) | 3 | 24 | 192 | (96, 32, 8) |
| (5, 2, 3, 4) | 4 | 120 | 960 | (192, 96, 32, 8) |
(1) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: dtype のメモリ比較 (難易度 ⭐)
import numpy as np
for dtype in [np.int8, np.int32, np.int64, np.float32, np.float64]:
a = np.zeros(1000, dtype=dtype)
print(f"{dtype.__name__:10s} itemsize={a.itemsize} nbytes={a.nbytes}")
# int8 itemsize=1 nbytes=1000
# int32 itemsize=4 nbytes=4000
# int64 itemsize=8 nbytes=8000
# float32 itemsize=4 nbytes=4000
# float64 itemsize=8 nbytes=8000
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(2) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: reshape がデータをコピーしないことを確認する (難易度 ⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int64)
b = a.reshape(3, 4)
print(a.shape, b.shape) # (12,) (3, 4)
print(a.strides, b.strides) # (8,) (32, 8)
print(a.nbytes, b.nbytes) # 96 96
b[0, 0] = 999
print(a[0]) # 999 — same memory!
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
7. 包括的な例:3次元配列の徹底解説
(1) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: 3×4×2の配列の完全な性質の解析(難易度 ⭐⭐⭐)
import numpy as np
a = np.arange(24, dtype=np.int64).reshape(3, 4, 2)
print("=== Full array ===")
print(a)
# [[[ 0 1]
# [ 2 3]
# [ 4 5]
# [ 6 7]]
# [[ 8 9]
# [10 11]
# [12 13]
# [14 15]]
# [[16 17]
# [18 19]
# [20 21]
# [22 23]]]
print(f"ndim: {a.ndim}") # 3
print(f"shape: {a.shape}") # (3, 4, 2)
print(f"size: {a.size}") # 24
print(f"dtype: {a.dtype}") # int64
print(f"itemsize:{a.itemsize}") # 8
print(f"nbytes: {a.nbytes}") # 192 (24 * 8)
print(f"strides: {a.strides}") # (64, 16, 8)
# axis 0: 4*2*8 = 64 bytes per step
# axis 1: 2*8 = 16 bytes per step
# axis 2: 1*8 = 8 bytes per step
# Verify strides manually
expected_strides = (
a.shape[1] * a.shape[2] * a.itemsize, # 4*2*8 = 64
a.shape[2] * a.itemsize, # 2*8 = 16
a.itemsize # 8
)
print(f"manual strides: {expected_strides}") # (64, 16, 8)
print(f"match: {a.strides == expected_strides}") # True
# axis reduction
print(a.sum(axis=0).shape) # (4, 2)
print(a.sum(axis=1).shape) # (3, 2)
print(a.sum(axis=2).shape) # (3, 4)
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
❓ よくある質問
📖 まとめ
- ndarray は、同質で、型が固定され、メモリが連続した N 次元配列です。
ndimは次元数、shapeは軸の長さのタプル、sizeは要素の総数です- 軸には、外側から内側に向かって番号が付けられます。axis=0 が最も外側の軸、axis=ndim-1 が最も内側の軸です。
stridesは、各軸に沿って移動するためのメモリオフセットを表します。reshape 関数は、基となるデータを変更しません。dtypeは要素の型を判定し、itemsize、nbytes = size * itemsizeを判定する- ndarrayの連続メモリモデルと、リストのポインタが飛び回るモデルとの違いが、パフォーマンスの差の根本的な原因となっている
📝 練習問題
-
初心者向け(難易度 ⭐):長さ 5000 の配列を、データ型が int8、int32、float32、float64 のそれぞれで 4 つ作成してください。各配列の
nbytesを出力し、データ型が異なるとメモリ使用量が異なる理由を説明してください。 -
中級(難易度 ⭐⭐):形状=(5, 3, 2)、データ型=int16 の ndarray が与えられたとき、そのストライドと nbytes を手計算で求め、その計算結果をコードを使って検証してください。
-
上級(難易度 ⭐⭐⭐):shape=(2,3,4) の ndarray がメモリ上でどのように配置されているかを、自分の言葉で説明してください。図を描き、ストライドにラベルを付け、各要素のメモリオフセットを示してください。