Numpy で配列をバイナリで保存する方法としては、
numpy.save(npy_filepath, arr)
が最も単純な方法です。 しかし、
- 非圧縮であるため、ファイルサイズが大きくなりがち
- 単一の配列オブジェクトのみ保存
であるため、大量の配列を保存する場合や、配列サイズが大きい場合に悩むことがあると思います。
そう言った場合に有効なのがnumpy.savez_compressed
であり、
- 1つのファイル(xxxx.npz)に、複数の配列を保存
- 圧縮により、ファイルサイズを削減
ができます。
この記事では、numpy.savez_compressed
の実用時に、知っておくと利用が捗るポイントを紹介します。
使い方/拡張子/保存形式について
numpy.savez_compressed
では、.npz
という拡張子のファイルに1つ以上のNumpy配列を保存します。
numpy.savez_compressed
で保存したファイルをnp.load()
すると、NpzFile オブジェクトが得られます。
NpzFile オブジェクトは辞書型であり、numpy.savez_compressed
で指定したキーワード引数がキーとなっており、キーを与えると保存した配列を取り出すことができます。
>>> test_array = np.random.rand(3, 2) >>> test_vector = np.random.rand(4) >>> np.savez_compressed('/tmp/123', a=test_array, b=test_vector) >>> loaded = np.load('/tmp/123.npz') >>> print(np.array_equal(test_array, loaded['a'])) True >>> print(np.array_equal(test_vector, loaded['b'])) True
numpy.savez_compressed — NumPy v1.17 Manual より
numpy.savez
と numpy.savez_compressed
実は、どちらも _savez
をラップしてます。
@array_function_dispatch(_savez_compressed_dispatcher) def savez_compressed(file, *args, **kwds): ... _savez(file, args, kwds, True)
@array_function_dispatch(_savez_dispatcher) def savez(file, *args, **kwds): ... _savez(file, args, kwds, False)
_savez()
は以下のようになっており、真値型の第4引数で圧縮をどうするか制御しています。
def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None): import zipfile ... if compress: compression = zipfile.ZIP_DEFLATED else: compression = zipfile.ZIP_STORED ...
zipfile --- ZIP アーカイブの処理 — Python 3.8.2 ドキュメント
numpy/npyio.py at v1.17.0 · numpy/numpy · GitHub
1つの配列だけを圧縮する/複数ファイルをキーワード引数なしで保存する
numpy.savez_compressed
で1つの配列だけを圧縮したい場合もあると思います。
そういったときに、
>>> test_array = np.random.rand(3, 2) >>> np.savez_compressed('/tmp/123', a=test_array) >>> loaded = np.load('/tmp/123.npz') >>> loaded['a']
とするのはちょっとだけ面倒です。
実は、キーワード引数を指定しない場合、NpzFile オブジェクトのキーワード引数は以下のように設定されます。
def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None): ... namedict = kwds for i, val in enumerate(args): key = 'arr_%d' % i if key in namedict.keys(): raise ValueError( "Cannot use un-named variables and keyword %s" % key) namedict[key] = val ...
つまり、引数が1つの場合は、"arr_0"がキーとなります。
同様に、二つ以上の可変長引数では、それぞれのキーは "arr_0", "arr_1", "arr_2"... となります。
>>> test_array = np.random.rand(3, 2) >>> np.savez_compressed('/tmp/123', test_array) >>> loaded = np.load('/tmp/123.npz') >>> test_array = loaded['arr_0']
圧縮性能
先ほどのコードを見ると、ZIPでの圧縮となっていることが分かります(ランレングス圧縮+ハフマン符号化)。
従って、値のバリエーションが少ない+同じ値が連続するほど、圧縮性能が高くなります。
簡易検証:
※全てnp.float32
で保存
- np.random.ramdom(10000) => 75,638 バイト
- np.ones(10000) => 278 バイト
- np.random.randint(10, size=10000) => 7,552 バイト
- np.random.randint(100, size=10000) => 13,742 バイト
もちろん一様乱数は最悪です。
出現する値のバリエーションが少ない、出現頻度に偏りがある、値が連続して出現する、配列がスパースといった場合に効いてきます