NumPyのmemmap徹底解説:大容量データ処理の秘訣

2025-06-06

numpy.memmap()はNumPyライブラリの非常に便利な機能で、ディスク上のファイルをメモリにマップ(割り当て)することができます。これにより、あたかも通常のNumPy配列であるかのように、ディスク上の大きなデータを直接操作できるようになります。

以下に、その主な特徴と利点、そして使い方を説明します。

numpy.memmap() とは何か?

numpy.memmap()は、オペレーティングシステムのメモリマップトファイル機能を利用して、ディスク上のバイナリファイルをNumPy配列として扱えるようにするクラスです。

  • NumPy配列としての操作
    memmapオブジェクトはNumPy配列のインターフェースを持つため、通常のNumPy配列と同様にスライス、インデックス、数学演算などを行うことができます。
  • 直接書き込み (Direct Writing)
    memmapオブジェクトに対して行った変更は、自動的に(または明示的にフラッシュすることで)ディスク上の元のファイルに書き込まれます。
  • 遅延読み込み (Lazy Loading)
    配列の特定の部分にアクセスしたときに初めて、その部分がディスクから読み込まれます。
  • メモリにロードしない
    ファイル全体をRAMに一度にロードするのではなく、必要な部分だけをオンデマンドでメモリに読み込みます。これは、非常に大きなファイル(RAMに収まらないファイル)を扱う際に特に有効です。

主な利点

  1. 大規模データ処理
    RAMに収まらないような巨大なデータファイルを効率的に処理できます。たとえば、テラバイト級の画像データやログファイルなど。
  2. 高速なディスクアクセス
    OSのファイルキャッシュ機構が利用されるため、通常のファイルI/Oよりも効率的で高速なアクセスが期待できます。
  3. データ共有
    同じファイルを複数のプロセスでメモリマップすることで、プロセス間でデータを効率的に共有できます。
  4. 永続性
    変更はディスクに永続的に保存されるため、プログラムを終了してもデータが失われることはありません。

使い方 (numpy.memmap() の引数)

numpy.memmap()の基本的なコンストラクタは以下のようになります。

numpy.memmap(filename, dtype=uint8, mode='r+', offset=0, shape=None, order='C')

主要な引数は以下の通りです。

  • order: オプション。多次元配列のバイトオーダー('C'または'F')。
  • shape: オプション。配列の形状(タプル)。例えば、(100, 200)のような形を指定します。shapeを指定しない場合、ファイルの残りの部分が1次元配列として扱われます。
  • offset: オプション。ファイル内でのデータの開始位置(バイト単位)。ファイルの先頭から特定のバイト数をスキップして読み込みたい場合に使います。
  • mode: オプション。ファイルを開くモード。
    • 'r' (読み込み専用): 既存のファイルを読み込み専用で開きます。ファイルが存在しない場合はエラーになります。
    • 'r+' (読み書き): 既存のファイルを読み書き可能で開きます。ファイルが存在しない場合はエラーになります。
    • 'w+' (読み書き、新規作成/上書き): 新しいファイルを読み書き可能で開きます。ファイルが存在する場合は上書きされます。
    • 'c' (コピーオンライト): 既存のファイルを読み書き可能で開きますが、変更はディスク上の元のファイルには書き込まれず、変更された部分のみメモリにコピーされます(一時的な変更)。
  • dtype: オプション。配列のデータ型。デフォルトはuint8。例えば、numpy.float32numpy.int64などを指定できます。
  • filename: 必須。メモリマップするファイルのパス(文字列)。

具体的な使用例

既存のファイルを読み込む

import numpy as np

# ダミーのバイナリファイルを作成 (例として)
data_to_write = np.arange(10, dtype=np.float32)
data_to_write.tofile("my_data.bin")

# my_data.bin をメモリマップして読み込む
mmap_array = np.memmap("my_data.bin", dtype=np.float32, mode='r')

print("読み込んだデータ:", mmap_array)
print("データ型:", mmap_array.dtype)
print("形状:", mmap_array.shape)

# 配列の一部にアクセス
print("最初の3要素:", mmap_array[:3])

# 不要になったら閉じる (明示的に閉じなくてもPythonのGCが処理するが、明示するとより確実)
del mmap_array

新しいファイルを作成し、書き込む

import numpy as np

# 新しいmemmapファイルを作成 (float32型で1000000個の要素)
# mode='w+' はファイルが存在すれば上書き、なければ新規作成
mmap_array_write = np.memmap("new_large_data.bin", dtype=np.float32, mode='w+', shape=(1000000,))

# 配列に値を書き込む
for i in range(1000000):
    mmap_array_write[i] = i * 0.1

# 変更をディスクにフラッシュ (明示的に書き込みを保証)
mmap_array_write.flush()

print("新しいmemmapファイルに書き込みました。")

# 閉じる
del mmap_array_write

# 後で読み込んで確認
mmap_array_read = np.memmap("new_large_data.bin", dtype=np.float32, mode='r')
print("読み込んだ最初の10要素:", mmap_array_read[:10])
print("読み込んだ最後の10要素:", mmap_array_read[-10:])
del mmap_array_read

既存のmemmap配列を変更する

import numpy as np

# 既存のmemmapファイルを読み書きモードで開く
mmap_array_modify = np.memmap("new_large_data.bin", dtype=np.float32, mode='r+')

# 特定の要素を変更
mmap_array_modify[0] = 999.99
mmap_array_modify[1000] = -123.45

# 変更をディスクにフラッシュ
mmap_array_modify.flush()

print("memmap配列の要素を変更し、ディスクにフラッシュしました。")

# 閉じる
del mmap_array_modify

# 再度読み込んで変更を確認
mmap_array_check = np.memmap("new_large_data.bin", dtype=np.float32, mode='r')
print("変更後の最初の要素:", mmap_array_check[0])
print("変更後の1000番目の要素:", mmap_array_check[1000])
del mmap_array_check
  • リソース管理
    使用しなくなったmemmapオブジェクトは、delで削除するか、参照がなくなるようにすることで、関連するファイルハンドルが解放されます。
  • データ型とオフセット
    dtypeoffsetを正しく指定することが非常に重要です。これらが正しくないと、データが破損したり、意図しない読み込みになったりする可能性があります。
  • 同期 (Flush)
    memmapオブジェクトへの変更は、通常はOSが管理するキャッシュを介してディスクに書き込まれますが、プログラムの終了時やflush()メソッドを呼び出したときに明示的にディスクに同期されます。重要な変更を行った場合は、明示的にflush()を呼び出すことをお勧めします。
  • ファイルサイズ
    shapedtypeに基づいて、ファイルサイズが自動的に計算されます。w+モードで新しいファイルを作成する場合、指定されたshapedtypeに応じたサイズが確保されます。
  • OS依存性
    memmapはオペレーティングシステムの機能に依存するため、OSの制限(同時に開けるファイル数、ファイルサイズの制限など)を受ける可能性があります。


numpy.memmap() の一般的なエラーとトラブルシューティング

numpy.memmap() は強力なツールですが、その特性上、いくつかの一般的な落とし穴があります。

FileNotFoundError / ファイルが見つからない

エラーメッセージ例

FileNotFoundError: [Errno 2] No such file or directory: 'non_existent_file.bin'

原因

  • mode='r'mode='r+' で開こうとしたファイルがまだ作成されていない。
  • ファイルパスが間違っている(スペルミス、相対パスの基準が間違っているなど)。
  • 指定されたファイルが存在しない。

トラブルシューティング

  • 新しいファイルを作成する場合は、mode='w+' または mode='c' を使用していることを確認してください。
  • 既存のファイルを読み込む場合は、そのファイルが実際に存在するかを確認してください。
  • ファイルパスが正しいことを確認してください。絶対パスを使用するか、現在の作業ディレクトリをos.getcwd()などで確認してください。

ValueError: cannot create a memory-map for file of zero size / サイズがゼロのファイル

エラーメッセージ例

ValueError: cannot create a memory-map for file of zero size

原因

  • mode='w+' で新しいファイルを作成しようとした際に、shape引数が指定されていないか、shapeが空のタプル () になっている。offset がファイルサイズを超えている場合も同様のエラーが発生することがあります。
  • mode='r'mode='r+' で開こうとしたファイルが空である。

トラブルシューティング

  • 新しいファイルを作成する場合は、shape引数に適切な形状(例えば (100,)(10, 10))を指定してください。offsetを適切に設定しているかも確認してください。
  • 既存のファイルの場合、ファイルサイズが0でないことを確認してください。

ValueError: cannot map file to memory: [Errno 22] Invalid argument / 不適切な引数

エラーメッセージ例

ValueError: cannot map file to memory: [Errno 22] Invalid argument

原因

  • オペレーティングシステムによるメモリマップの制限(非常に大きなファイルや特殊なファイルシステムなど)。
  • shapeNoneで、かつファイルが空である場合(前述の「サイズがゼロのファイル」と関連)。
  • offsetがファイルの境界を超えている、または負の値である。
  • dtypeshapeの組み合わせが、指定されたファイルの実際のサイズと一致しない。特にoffsetを指定している場合に起こりやすい。

トラブルシューティング

  • offsetが正しく設定されていることを確認してください。特にヘッダーを持つファイルなど、特定のバイト数だけスキップして読み込む場合に重要です。
  • 既存のファイルを読み込む場合、shapeを省略して1次元配列として読み込み、後でreshapeすることも検討してください。
  • ファイルの実際のバイトサイズを確認し、dtype.itemsize * np.prod(shape) + offset がそのファイルサイズを超えていないか確認してください。

OSError: [Errno 28] No space left on device / ディスク容量不足

エラーメッセージ例

OSError: [Errno 28] No space left on device

原因

  • 新しい memmap ファイルを作成しようとしたが、ディスクに十分な空き容量がない。

トラブルシューティング

  • ディスクの空き容量を確認し、不要なファイルを削除するなどして容量を確保してください。

データの不整合・意図しない値

症状

  • 他のプログラムやC言語のmmapと連携させたときに値がずれる。
  • memmapを通して読み書きしたデータが、期待した値と異なる。

原因

  • flush()の不足
    'r+''w+' モードで書き込んだ変更が、明示的にflush()を呼び出していないため、まだディスクに書き込まれていない可能性がある。
  • shapeの誤り
    特に多次元配列の場合、shapeが間違っていると、要素の配置が期待と異なる。
  • offsetの誤り
    ヘッダーなど、スキップすべきバイト数を間違えている。
  • バイトオーダーの不一致
    異なるシステム間や異なる言語間でファイルを受け渡す場合に、バイトオーダー(エンディアン)が異なるため、データが正しく解釈されない。NumPyはデフォルトでシステムのバイトオーダーを使用しますが、memmapはファイルのバイトオーダーを直接読み取ります。
  • dtypeの不一致
    ファイルに保存されているデータの実際の型と、memmap作成時に指定したdtypeが異なる。例えば、ファイルがfloat64で書かれているのにfloat32で読み込んでいる場合など。

トラブルシューティング

  • flush()の呼び出し
    書き込みモード ('r+', 'w+') で使用している場合は、重要な変更後に必ずmmap_array.flush()を呼び出してディスクへの同期を保証してください。プログラム終了時やdel mmap_arrayでも自動的に行われることが多いですが、明示的に行うのが安全です。
  • バイトオーダーの考慮
    異なるシステム間でファイルを共有する場合、dtypeの後に.newbyteorder()などを使用してバイトオーダーを明示的に指定することを検討してください。
    # 例: ビッグエンディアンのfloat32として読み込む
    mmap_array = np.memmap("my_data.bin", dtype='>f4', mode='r')
    # または
    mmap_array = np.memmap("my_data.bin", dtype=np.dtype(np.float32).newbyteorder('>'), mode='r')
    
  • dtypeとoffsetを厳密に確認
    元のファイルがどのように生成されたかを把握し、それと完全に一致するdtypeoffsetを指定してください。

MemoryError / メモリ不足

エラーメッセージ例

MemoryError: Unable to allocate ... bytes for an array

原因

  • memmapオブジェクト自体は小さいですが、その上にNumPyの通常の配列操作(例: np.sum(), np.mean()など)を適用すると、内部で一時的な配列が生成され、それが大量のメモリを消費することがあります。
  • memmapはRAMに全データをロードしないとはいえ、オペレーティングシステムは仮想アドレス空間を割り当てます。非常に大きなファイルに対してmemmapを作成しようとしたときに、仮想メモリが不足することがあります。特に32ビットシステムではこの問題が顕著です。

トラブルシューティング

  • NumPyのUFuncは通常、memmapに対してもメモリ効率的に動作しますが、複雑な操作や大量の中間結果を生成するような操作は避けるか、注意深くメモリ使用量を確認してください。
  • 可能な限り、memmapオブジェクトに対して一括で処理を行うのではなく、チャンク(部分)ごとに処理を行うようにコードを工夫してください。
  • 64ビットシステムを使用していることを確認してください。

ファイルがロックされてアクセスできない (PermissionError, OSError)

エラーメッセージ例

PermissionError: [Errno 13] Permission denied: 'locked_file.bin'
OSError: [WinError 32] The process cannot access the file because it is being used by another process: 'locked_file.bin'

原因

  • ファイルのアクセス権限がない。
  • 他のプログラムや同じPythonスクリプトの別の部分で、既にファイルが開かれている。
  • 特にWindows環境では、ファイルが別のプロセスによってロックされることがよくあります。必要であれば、del mmap_array を使って明示的にmemmapオブジェクトを削除し、ファイルハンドルを解放してください。
  • ファイルに適切な読み書き権限があることを確認する。
  • ファイルを開いている可能性のある他のアプリケーションを閉じる。
  • 小さなファイルでテスト
    大きなファイルで問題が発生した場合は、同じデータ型と構造を持つ小さなテストファイルを作成し、そこで動作を確認することで、問題の切り分けがしやすくなります。
  • ログ出力
    データの読み込み前後にprint文やロギング機能を使って、dtype, shape, offset, ファイルサイズなどを確認し、期待通りの値になっているか検証してください。
  • withステートメントの検討
    memmapはPythonのファイルオブジェクトのようにwithステートメントで直接扱うことはできませんが、mmapモジュールのmmapクラスはwithステートメントをサポートしています。NumPyのmemmapは、内部的にPythonのmmapを使用しています。明示的にdelするか、関数のスコープを抜けてガベージコレクションによって解放されるのを待つことで、リソースは解放されます。
  • try-exceptブロックの使用
    ファイルの操作は予期せぬエラーが発生しやすいため、try-exceptブロックでエラーを捕捉し、適切なエラーハンドリングを行うことが重要です。


numpy.memmap() は、ディスク上のファイルをNumPy配列として扱うための強力なツールです。ここでは、基本的な使い方から応用例まで、いくつかのコード例を挙げながら解説します。

例1: 新しいメモリマップドファイルを作成し、書き込む

この例では、新しいファイルを作成し、そこにデータを書き込みます。

import numpy as np
import os

# 1. ファイル名と配列の仕様を定義
filename = "my_large_array.bin"
data_dtype = np.float64 # データ型をdouble (64ビット浮動小数点) とする
array_shape = (1000, 1000) # 1000x1000の2次元配列とする

# 2. mode='w+' で新しいメモリマップドファイルを作成
#    'w+': 書き込み・読み込みモードでファイルを開く。
#          ファイルが存在しない場合は新規作成。存在する場合は内容を上書き。
print(f"新しいメモリマップドファイル '{filename}' を作成中...")
mmap_write = np.memmap(filename, dtype=data_dtype, mode='w+', shape=array_shape)

# 3. 配列にデータを書き込む
#    通常のNumPy配列と同じようにインデックスアクセスやスライスが可能
print("データを書き込み中...")
for i in range(array_shape[0]):
    for j in range(array_shape[1]):
        mmap_write[i, j] = i * array_shape[1] + j # 適当なデータを生成

# 4. 変更をディスクにフラッシュ (強制的に書き込む)
#    これを呼び出すことで、メモリ上の変更がディスクに確実に反映されます。
#    プログラム終了時やdel mmap_write でもフラッシュされますが、明示が安全。
mmap_write.flush()
print("データの書き込みとフラッシュが完了しました。")

# 5. 不要になったらメモリマップドオブジェクトを削除
#    これによりファイルハンドルが解放されます。
del mmap_write

# ファイルが作成されたことを確認
file_size_bytes = os.path.getsize(filename)
expected_size_bytes = data_dtype.itemsize * array_shape[0] * array_shape[1]
print(f"作成されたファイルのサイズ: {file_size_bytes} バイト")
print(f"期待されるファイルのサイズ: {expected_size_bytes} バイト")
assert file_size_bytes == expected_size_bytes

解説

  • flush() は、メモリ上の変更がディスクに書き込まれることを保証するために重要です。
  • mmap_write[i, j] = ... のように、あたかも通常のNumPy配列であるかのように要素にアクセスし、値を設定できます。
  • mode='w+' を使用することで、指定された shapedtype に基づいてファイルが作成(または上書き)され、必要なディスク領域が確保されます。

例2: 既存のメモリマップドファイルを読み込む

例1で作成したファイルを読み込んで、データにアクセスします。

import numpy as np
import os

filename = "my_large_array.bin"
data_dtype = np.float64
array_shape = (1000, 1000)

# 1. ファイルが存在するか確認
if not os.path.exists(filename):
    print(f"エラー: ファイル '{filename}' が見つかりません。例1を実行してください。")
else:
    # 2. mode='r' で既存のメモリマップドファイルを読み込み専用で開く
    #    'r': 読み込み専用モード。ファイルが存在しない場合はエラー。
    print(f"既存のメモリマップドファイル '{filename}' を読み込み中...")
    mmap_read = np.memmap(filename, dtype=data_dtype, mode='r', shape=array_shape)

    # 3. データにアクセス (読み込み専用)
    print("最初の5x5要素:")
    print(mmap_read[:5, :5])

    print("\n中央の要素 (例: mmap_read[499, 499]):")
    print(mmap_read[499, 499])

    # 4. 配列の基本的な情報を確認
    print(f"\n形状: {mmap_read.shape}")
    print(f"データ型: {mmap_read.dtype}")
    print(f"オフセット: {mmap_read.offset}") # デフォルトは0

    # 5. 不要になったらメモリマップドオブジェクトを削除
    del mmap_read
    print("\n読み込みが完了しました。")

# (オプション) ファイルを削除してクリーンアップ
# os.remove(filename)
# print(f"ファイル '{filename}' を削除しました。")

解説

  • shape を指定することで、ファイルが多次元配列として扱われます。shape を省略した場合は1次元配列として扱われます。
  • mode='r' で開くと、ファイルは読み込み専用となり、誤ってデータを変更するのを防ぎます。

例3: 既存のメモリマップドファイルを変更する

既存のファイルを開き、そのデータを更新します。

import numpy as np
import os

filename = "my_large_array.bin"
data_dtype = np.float64
array_shape = (1000, 1000)

if not os.path.exists(filename):
    print(f"エラー: ファイル '{filename}' が見つかりません。例1を実行してください。")
else:
    # 1. mode='r+' で既存のメモリマップドファイルを読み書き可能で開く
    #    'r+': 読み書きモード。ファイルが存在しない場合はエラー。
    print(f"既存のメモリマップドファイル '{filename}' を読み書きモードで開いています...")
    mmap_modify = np.memmap(filename, dtype=data_dtype, mode='r+', shape=array_shape)

    # 2. 特定の要素を変更
    print(f"変更前の mmap_modify[0, 0]: {mmap_modify[0, 0]}")
    mmap_modify[0, 0] = 999.999 # 最初の要素を変更

    print(f"変更前の mmap_modify[500, 500]: {mmap_modify[500, 500]}")
    mmap_modify[500, 500] = -123.456 # 中央の要素を変更

    # 3. 変更をディスクにフラッシュ
    mmap_modify.flush()
    print("データの変更とフラッシュが完了しました。")

    # 4. 不要になったらメモリマップドオブジェクトを削除
    del mmap_modify

    # 5. 変更が反映されたか確認するために再度読み込む
    print("変更を確認するため、ファイルを再度読み込みます...")
    mmap_verify = np.memmap(filename, dtype=data_dtype, mode='r', shape=array_shape)
    print(f"変更後の mmap_verify[0, 0]: {mmap_verify[0, 0]}")
    print(f"変更後の mmap_verify[500, 500]: {mmap_verify[500, 500]}")
    del mmap_verify

解説

  • 変更後には必ず flush() を呼び出すことで、ディスクに確実に書き込まれます。
  • mode='r+' を使うことで、既存のファイルを読み書き両方で操作できます。

例4: ファイルの途中のデータからメモリマップを作成する (offset の使用)

ファイルにヘッダーなどがあり、実際のデータが途中のオフセットから始まる場合に有用です。

import numpy as np
import os

filename_offset = "data_with_header.bin"
header_size_bytes = 100 # 仮のヘッダーサイズ
data_dtype = np.int32
num_elements = 5000 # データ部分の要素数

# 1. ヘッダー付きのダミーファイルを作成
print(f"ヘッダー付きのダミーファイル '{filename_offset}' を作成中...")
with open(filename_offset, 'wb') as f:
    f.write(b'\x00' * header_size_bytes) # 100バイトのダミーヘッダー
    # その後に実際のデータを書き込む
    np.arange(num_elements, dtype=data_dtype).tofile(f)
print("ダミーファイルの作成が完了しました。")

# 2. ヘッダーをスキップしてメモリマップを作成
#    offset=header_size_bytes でデータ開始位置を指定
mmap_offset = np.memmap(filename_offset, dtype=data_dtype, mode='r',
                        offset=header_size_bytes, shape=(num_elements,))

# 3. データにアクセス
print("メモリマップドデータの最初の10要素:")
print(mmap_offset[:10]) # 0から始まるはず

print("メモリマップドデータの最後の10要素:")
print(mmap_offset[-10:]) # 4990から始まるはず

# 4. 不要になったら削除
del mmap_offset

# (オプション) ファイルを削除してクリーンアップ
# os.remove(filename_offset)
# print(f"ファイル '{filename_offset}' を削除しました。")

解説

  • offset 引数にバイト数を指定することで、ファイルの先頭からそのバイト数だけスキップして、memmap の開始位置とすることができます。これは、ファイルにメタデータやヘッダーが含まれている場合に非常に便利です。

例5: 異なるプロセス間でのデータ共有

numpy.memmap() は、複数のPythonプロセス間で同じファイルをメモリマップすることで、効率的なデータ共有を可能にします。

プロセス1 (書き込み側)

# writer_process.py
import numpy as np
import os
import time

filename_shared = "shared_data.bin"
shared_dtype = np.int32
shared_shape = (100,) # 100個の整数

# 新しい共有ファイルを作成 (上書き)
print("Writer: 共有ファイルを作成中...")
mmap_shared_write = np.memmap(filename_shared, dtype=shared_dtype, mode='w+', shape=shared_shape)

for i in range(10):
    value = i * 100
    mmap_shared_write[0] = value # 最初の要素を更新
    mmap_shared_write.flush() # 変更をディスクにフラッシュ
    print(f"Writer: mmap_shared_write[0] を {value} に更新しました。")
    time.sleep(1) # 1秒待機

print("Writer: 完了しました。")
del mmap_shared_write

プロセス2 (読み込み側)

# reader_process.py
import numpy as np
import os
import time

filename_shared = "shared_data.bin"
shared_dtype = np.int32
shared_shape = (100,)

# ファイルが存在するまで待機
while not os.path.exists(filename_shared):
    print("Reader: ファイルを待機中...")
    time.sleep(1)

# 共有ファイルを読み込み専用で開く
print("Reader: 共有ファイルを読み込み中...")
mmap_shared_read = np.memmap(filename_shared, dtype=shared_dtype, mode='r', shape=shared_shape)

for _ in range(10):
    # 最初の要素の値を読み込む
    # memmapはOSレベルでキャッシュされるため、変更が反映されやすい
    current_value = mmap_shared_read[0]
    print(f"Reader: 現在の mmap_shared_read[0]: {current_value}")
    time.sleep(1) # 1秒待機

print("Reader: 完了しました。")
del mmap_shared_read

# (オプション) 共有ファイルを削除してクリーンアップ
# os.remove(filename_shared)
# print(f"共有ファイル '{filename_shared}' を削除しました。")

実行方法

  1. writer_process.py を実行します。
  2. 別のターミナルを開き、reader_process.py を実行します。
  3. reader_process.pywriter_process.py によって変更される値をリアルタイムで読み取るのが確認できます。
  • writer_process.pyflush() を呼び出すことで、変更がディスク(およびOSのファイルキャッシュ)に書き込まれ、reader_process.py がその変更をほぼリアルタイムで読み取ることができます。
  • 2つの異なるPythonプロセスが同じ物理ファイルをメモリマップしています。


主な代替方法を以下に示します。

numpy.memmap() の代替方法

ファイル全体をRAMにロードする (np.load(), np.fromfile(), pandasなど)

データがRAMに十分に収まる場合、最もシンプルで高速な方法です。

  • Pandasを使用
    • CSV, Parquet, HDF5 などの構造化データフォーマットを扱う場合、Pandasは非常に強力です。特にParquetやHDF5は、大規模な数値データを効率的に保存・ロードできます。
      import pandas as pd
      import numpy as np
      
      # データ生成 (例: DataFrame)
      df = pd.DataFrame(np.random.rand(1000, 5), columns=[f'col_{i}' for i in range(5)])
      
      # Parquet形式で保存 (列指向ストレージで効率的)
      df.to_parquet("my_dataframe.parquet")
      
      # Parquet形式からロード
      loaded_df = pd.read_parquet("my_dataframe.parquet")
      print("PandasでロードしたDataFrame:", loaded_df.shape)
      
  • NumPyの機能を使用
    • .npy / .npz ファイル
      NumPyが独自に定義するバイナリ形式で、配列の形状やdtypeなどのメタデータも保存されます。np.save() で保存し、np.load() でロードします。最も推奨されるNumPy配列の永続化方法です。
      import numpy as np
      
      # データ生成
      data = np.random.rand(1000, 1000)
      
      # 保存
      np.save("my_array.npy", data)
      
      # ロード
      loaded_data = np.load("my_array.npy")
      print("np.load()でロードしたデータ:", loaded_data.shape, loaded_data.dtype)
      
    • 生バイナリファイル
      np.tofile() で生バイト列として保存し、np.fromfile() で読み込みます。この場合、dtypeやshapeは別途指定する必要があります。
      import numpy as np
      
      # データ生成
      data = np.arange(1000, dtype=np.float32)
      
      # 生バイナリファイルに保存
      data.tofile("raw_data.bin")
      
      # 生バイナリファイルからロード (dtypeとshapeを指定)
      loaded_raw_data = np.fromfile("raw_data.bin", dtype=np.float32)
      print("np.fromfile()でロードしたデータ:", loaded_raw_data.shape, loaded_raw_data.dtype)
      

利点

  • データ全体がメモリにあるため、ランダムアクセスが非常に高速。
  • NumPyやPandasの強力な機能をフル活用できる。
  • 操作がシンプルで、通常のNumPy配列と同様に扱える。

欠点

  • データがRAMサイズを超えると、この方法は使えない。

チャンク処理 (Chunking)

データがRAMに収まらないが、memmapの特定の制限(ファイルがロックされる、OS依存性など)を回避したい場合に有効です。ファイルを小さな「チャンク(塊)」に分割して読み込み、処理し、結果を結合します。

import numpy as np
import os

# 大規模なダミーファイルを生成 (例: float32で1GB)
num_elements = 250_000_000 # float32 * 250M = 1GB
large_filename = "very_large_data.bin"

if not os.path.exists(large_filename):
    print(f"大規模ダミーファイル '{large_filename}' を作成中...")
    dummy_data = np.arange(num_elements, dtype=np.float32)
    dummy_data.tofile(large_filename)
    del dummy_data # メモリを解放
    print("作成完了。")

# チャンクサイズを定義 (例: 10MBずつ読み込む)
chunk_size_bytes = 10 * 1024 * 1024 # 10 MB
item_size = np.dtype(np.float32).itemsize
chunk_elements = chunk_size_bytes // item_size # チャンクごとの要素数

total_elements = os.path.getsize(large_filename) // item_size

processed_sum = 0.0

print(f"ファイルをチャンクで処理中... (総要素数: {total_elements}, チャンク要素数: {chunk_elements})")
for i in range(0, total_elements, chunk_elements):
    start_index = i
    end_index = min(i + chunk_elements, total_elements)
    
    # ファイルからチャンクを読み込む
    # np.fromfile は offset と count を指定できる
    current_chunk = np.fromfile(large_filename, dtype=np.float32,
                                offset=start_index * item_size,
                                count=end_index - start_index)
    
    # チャンクを処理 (例: 合計を計算)
    processed_sum += np.sum(current_chunk)
    print(f"  チャンク {start_index}-{end_index-1} を処理しました。")

print(f"チャンク処理の合計: {processed_sum}")

# (オプション) ファイルを削除してクリーンアップ
# os.remove(large_filename)

利点

  • メモリ使用量を細かく制御できる。
  • memmapのようなファイルロックやOS依存性の問題を回避できる場合がある。
  • RAMに全データをロードせずに大規模データを処理できる。

欠点

  • データの結合が必要な場合がある。
  • コードが複雑になる。
  • ランダムアクセスが非効率。特定の要素にアクセスするには、その要素を含むチャンクを読み込む必要がある。

データベース (HDF5, Zarr, SQLiteなど)

構造化された大規模なデータや、複雑なクエリが必要な場合に適しています。

  • SQLite
    リレーショナルデータベースですが、BLOB(バイナリラージオブジェクト)としてバイナリデータを保存することも可能です。NumPy配列を直列化して保存・取得できます。ただし、数値配列のランダムアクセス性能はHDF5やZarrほどは期待できません。

  • Zarr
    クラウドストレージや並列処理に適した、チャンク化された多次元配列のストレージ形式です。HDF5のコンセプトを現代的に再考したもので、柔軟性とスケーラビリティが高いです。

  • HDF5 (H5Py, PyTables)
    階層的なデータ構造を持つファイル形式で、大規模な数値データを効率的に保存・管理できます。部分的な読み書きや圧縮もサポートします。memmapと似ていますが、より高レベルな機能を提供します。

    import h5py
    import numpy as np
    
    h5_filename = "my_large_data.h5"
    array_shape = (10000, 1000) # 10M要素の配列
    
    # HDF5ファイルにデータを保存
    print(f"HDF5ファイル '{h5_filename}' にデータを保存中...")
    with h5py.File(h5_filename, 'w') as f:
        # 'data'という名前のデータセットを作成
        dset = f.create_dataset('data', shape=array_shape, dtype=np.float32)
        # チャンクごとにデータを書き込むことも可能 (パフォーマンス向上)
        # dset = f.create_dataset('data', shape=array_shape, dtype=np.float32, chunks=(1000, 1000))
    
        # データを書き込む (例: 一部だけ)
        dset[0:100, :] = np.random.rand(100, array_shape[1])
        dset[9000:9100, :] = np.random.rand(100, array_shape[1])
    print("HDF5ファイルへの保存が完了しました。")
    
    # HDF5ファイルからデータを読み込む
    print(f"HDF5ファイル '{h5_filename}' からデータを読み込み中...")
    with h5py.File(h5_filename, 'r') as f:
        loaded_dset = f['data']
        # 部分的に読み込む
        chunk = loaded_dset[0:5, 0:5]
        print("HDF5から読み込んだ最初の5x5要素:\n", chunk)
    
        # データセット全体の形状や型も取得可能
        print(f"HDF5データセットの形状: {loaded_dset.shape}, データ型: {loaded_dset.dtype}")
    

利点

  • 並列処理や分散処理に適している場合がある。
  • 複雑なクエリやデータ構造をサポート。
  • 部分的な読み書き、圧縮、メタデータの管理が可能。
  • 非常に大規模なデータセットに対応できる。

欠点

  • オーバーヘッドが発生し、非常に単純なファイルアクセスではmemmapやチャンク処理より遅くなる場合がある。
  • NumPy配列としての直接操作性が若干失われる(専用のAPIを介す必要がある)。
  • 設定や管理がmemmapよりも複雑になる。

Dask Arrays

Daskは、NumPyライクなAPIを提供しつつ、ディスクに収まらない配列を処理するためのライブラリです。複数のNumPy配列のチャンクを組み合わせて大規模な配列を表現し、必要に応じてデータをディスクからロードします。memmapと類似の思想ですが、より高レベルで柔軟な計算グラフを構築できます。

import dask.array as da
import numpy as np
import os

# Dask Array を使用して大規模な配列を扱う
# Dここでは、ディスク上のダミーファイルをデータ源とする

# 大規模なダミーファイルを生成 (例: float64で2GB)
num_elements_dask = 250_000_000 # float64 * 250M = 2GB
dask_filename = "dask_large_data.bin"

if not os.path.exists(dask_filename):
    print(f"Dask用大規模ダミーファイル '{dask_filename}' を作成中...")
    dummy_data = np.arange(num_elements_dask, dtype=np.float64)
    dummy_data.tofile(dask_filename)
    del dummy_data
    print("作成完了。")

# Dask Array を生成 (memmapをバックエンドとして利用可能)
# chunksize を指定することで、どのようにデータをチャンクに分割して扱うかをDaskに伝える
# np.memmap を直接 Dask Array に変換することも可能
mmap_base = np.memmap(dask_filename, dtype=np.float64, mode='r', shape=(num_elements_dask,))
dask_array = da.from_array(mmap_base, chunks=(10_000_000,)) # 10M要素ごとにチャンク化

print(f"Dask Arrayの形状: {dask_array.shape}, データ型: {dask_array.dtype}")
print(f"Dask Arrayのチャンクサイズ: {dask_array.chunksize}")

# Dask Arrayに対してNumPyライクな操作を実行
# 実際の計算は、必要になったときに(.compute()を呼び出したときなど)行われる
mean_value = dask_array.mean().compute()
print(f"Dask Arrayの平均値: {mean_value}")

# 部分的なアクセスも可能
subset = dask_array[1000:1010].compute()
print(f"Dask Arrayの部分配列 (1000-1009): {subset}")

del mmap_base # Dask Arrayが参照している限り、ファイルは開かれたまま

利点

  • メモリ使用量を最適化し、必要なデータチャンクのみをロードする。
  • 並列処理や分散処理を容易に設定できる。
  • NumPyライクなAPIで、大規模なデータセットを扱うための計算グラフを自動的に構築する。

欠点

  • 複雑な操作では、計算グラフの最適化を理解する必要がある場合がある。
  • NumPyの直接的な操作に比べてオーバーヘッドがある。

numpy.memmap() とこれらの代替方法のどれを選ぶべきかは、以下の要因によって異なります。

  • 並列処理/分散処理
    複数のプロセスやマシンでデータを共有・処理する必要があるか?
  • 永続性
    データを永続的に保存する必要があるか?
  • 複雑さの許容度
    開発の容易さやメンテナンス性。
  • パフォーマンス要件
    厳密なリアルタイム性能が必要か?
  • データの構造
    単純な数値配列か?構造化されたテーブルデータか?階層的なデータか?
  • アクセスパターン
    シーケンシャルアクセスか?ランダムアクセスか?
  • データサイズ
    RAMに収まるか?