データ分析や機械学習の現場において、PythonのNumPyは欠かせないライブラリの一つです。
大規模なデータセットを扱う際、プログラムがどの程度のメモリを消費しているかを正確に把握することは、システムの安定稼働やパフォーマンス向上のために極めて重要です。
特に、メモリリソースが限られた環境や、数千万行を超えるデータを処理する場合には、わずかな型選択の違いが大きな影響を及ぼします。
本記事では、NumPy配列のメモリサイズを確認するためのnbytes属性の使い方と、Python標準のsys.getsizeofとの決定的な違いについて詳しく解説します。
NumPy配列のメモリサイズを取得する基本:nbytes属性
NumPy配列(ndarray)のメモリ使用量を確認する最も直感的で正確な方法は、nbytes属性を使用することです。
この属性は、配列の要素が占有する合計バイト数を返します。
まずは、基本的なnbytesの使用例を確認してみましょう。
import numpy as np
# 1000個の要素を持つfloat64型の配列を作成
data = np.ones(1000, dtype=np.float64)
# メモリサイズを表示
print(f"配列のサイズ: {data.nbytes} bytes")
配列のサイズ: 8000 bytes
上記の例では、64ビット(8バイト)の浮動小数点数が1000個並んでいるため、合計で8000バイトのメモリを消費していることがわかります。
nbytesは、配列の全要素のサイズを計算する際に、「要素の個数(size)」と「一つの要素あたりのサイズ(itemsize)」を掛け合わせた値を出力します。
itemsizeとsizeの理解
nbytesの中身をより深く理解するために、関連するプロパティについても知っておく必要があります。
size属性は配列内の要素の総数を表し、itemsize属性は1つの要素が占めるバイト数を表します。
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
print(f"要素数 (size): {arr.size}")
print(f"1要素のサイズ (itemsize): {arr.itemsize}")
print(f"合計サイズ (nbytes): {arr.nbytes}")
要素数 (size): 6
1要素のサイズ (itemsize): 4
合計サイズ (nbytes): 24
このように、nbytes = size * itemsize という関係性が成り立っています。
多次元配列であっても、メモリ上に確保される実データの総量は、この計算式で求めることができます。
nbytesとsys.getsizeofの決定的な違い
Pythonでオブジェクトのサイズを調べる際、多くの開発者が最初に思い浮かべるのはsys.getsizeof()関数かもしれません。
しかし、NumPy配列のサイズを確認する場合、sys.getsizeof()の使用には注意が必要です。
なぜなら、これら二つが返す数値には明確な定義の違いがあるからです。
sys.getsizeofが返す値の正体
sys.getsizeof()は、Pythonオブジェクトそのものが消費するメモリサイズを返します。
これには、配列のデータ本体だけでなく、「次元数」「形状(shape)」「データ型(dtype)」などのメタ情報(オーバーヘッド)が含まれます。
import numpy as np
import sys
arr = np.zeros(1000)
print(f"nbytesの結果: {arr.nbytes}")
print(f"sys.getsizeofの結果: {sys.getsizeof(arr)}")
nbytesの結果: 8000
sys.getsizeofの結果: 8112
実行結果を見ると、sys.getsizeofの方がわずかに大きな値を返していることがわかります。
この差分が、NumPyのndarrayオブジェクトとしての管理情報に必要なメモリです。
どちらを使うべきか
結論として、純粋にデータとしてのメモリ消費量を知りたい場合は、nbytesを使用するのが正解です。
特に数GB単位の巨大な配列を扱う場合、メタ情報のサイズは無視できるほど小さくなります。
しかし、もう一つ重要な注意点があります。
sys.getsizeof()は、配列が他の配列の「ビュー(view)」である場合、その背後にある実際のデータバッファのサイズを考慮しません。
一方でnbytesは、そのビューが表示している範囲のみのサイズを正確に計算します。
データ型(dtype)によるメモリサイズの劇的な変化
NumPyを利用する最大のメリットの一つは、データの型を細かく指定できることです。
適切なデータ型を選択することで、メモリ使用量を半分以下に削減することも可能です。
以下の表は、同じ要素数を持つ配列でも、型によってどれほどサイズが変わるかを示しています。
| データ型 (dtype) | 1要素のサイズ | 100万要素のサイズ |
|---|---|---|
| int8 / uint8 | 1 byte | 約 1 MB |
| int32 / float32 | 4 bytes | 約 4 MB |
| int64 / float64 | 8 bytes | 約 8 MB |
| complex128 | 16 bytes | 約 16 MB |
デフォルト設定では多くの環境でint64やfloat64が採用されますが、値の範囲が限定されている場合には、より小さな型を指定しましょう。
例えば、0から255までの整数しか扱わない画像データであれば、uint8を使用することでメモリ消費を1/8に抑えられます。
型変換によるメモリ節約の例
実際に型変換(キャスト)を行って、メモリサイズがどれほど変化するかを確認してみましょう。
import numpy as np
# デフォルトのfloat64で作成
arr_large = np.random.rand(1000000)
print(f"float64のサイズ: {arr_large.nbytes / (1024**2):.2f} MB")
# float32に変換
arr_small = arr_large.astype(np.float32)
print(f"float32のサイズ: {arr_small.nbytes / (1024**2):.2f} MB")
float64のサイズ: 7.63 MB
float32のサイズ: 3.81 MB
このように、用途に合わせて最適なdtypeを選択することが、メモリ管理の第一歩となります。
実務で役立つメモリ使用量の確認テクニック
実際の開発プロジェクトでは、個別の配列だけでなく、スクリプト全体のメモリ使用量を監視したい場面があります。
特にJupyter Notebookなどで試行錯誤していると、不要になった巨大な配列がメモリを占有し続けることがよくあります。
不要な配列の解放
NumPy配列が不要になったら、Pythonのdel文を使用して変数を削除し、必要に応じてgc.collect()でガベージコレクションを明示的に呼び出すのが効果的です。
しかし、変数を消しただけではメモリが即座にOSへ返却されないこともあるため注意が必要です。
大規模データのメモリプロファイリング
より高度な分析には、memory_profilerのような外部ライブラリを併用することをお勧めします。
これにより、行単位でのメモリ消費の推移を可視化でき、どの処理がボトルネックになっているかを特定できます。
また、np.memmapを利用することで、メモリに乗り切らない巨大なファイルをディスク上に置いたまま、配列として操作する手法も存在します。
この場合、nbytesは論理的なサイズを示しますが、実際に物理メモリ(RAM)を消費している量とは異なります。
仮想メモリの概念が絡むため、大規模データ処理では「論理サイズ」と「物理消費量」を切り分けて考える癖をつけましょう。
まとめ
本記事では、NumPy配列のメモリサイズを確認する方法について解説しました。
最も正確に配列のデータサイズを知るには、nbytes属性を使用するのがベストです。
sys.getsizeof()はオブジェクトの管理情報を含んだサイズを返しますが、配列の実データ部分に焦点を当てる場合は、nbytesの方が実用的です。
また、メモリ消費を抑えるためには、適切なデータ型(dtype)を選択することが何よりも重要です。
大規模なデータを扱う際には、常にnbytesを確認し、効率的なコード設計を心がけてください。
こうした細かいメモリ管理の積み重ねが、最終的なプログラムの信頼性とスピードに繋がります。
