【Python】測定配列を形と型を保って保存する

PythonのTopに戻る

結論:名前付きnpzへ保存し、読込後にshapeとdtypeを確かめる

複数のNumPy配列を一式として保存したいなら、np.savezへ名前付き引数で渡すと扱いやすい。値だけでなく配列の形とdtypeも保存されるので、2次元の測定値、時刻、チャネル名をまとめて往復できる。CSVのように行列をテキストへ展開する用途とは異なり、Python側の配列構造をそのまま次の処理へ持ち越したい場面に向いている。

保存できたことと、正しいデータを読めたことは分けて確認しよう。ファイルを取り違えれば、形式が正しくても別の実験を解析してしまう。キー一覧、shape、dtype、座標との長さの関係を読込時に確かめると、古いファイルや列順の違いに早く気付ける。測定単位や軸の意味は配列の形だけでは分からないので、別途明示する必要がある。

そのまま動かせる例

例は2時刻×3チャネルのfloat32測定配列、float64の時刻、文字列のチャネル名を作り、一時フォルダーへ保存して読み直す。NumPyが利用できれば外部のデータなしで実行できる。一時ファイルは処理終了時に削除されるため、保存形式の動作だけを安全に試せる。実際に残すときはpathを自分の保存先へ変更し、既存ファイルの上書きに注意しよう。

後半ではobject配列を意図的に作り、allow_pickle=Falseでの読込が拒否されることを確認している。これは想定したエラーの例で、例外を握りつぶして本処理を続けるための書き方ではない。本文の測定データと文字列配列にはobjectを使わず、pickleなしで読み書きできる形にそろえている。

from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np

signal = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32)
time = np.array([0.0, 0.5], dtype=np.float64)
channels = np.array(["A", "B", "C"])
with TemporaryDirectory() as folder:
    path = Path(folder) / "measurement.npz"
    np.savez(path, signal=signal, time=time, channels=channels)
    with np.load(path, allow_pickle=False) as loaded:
        assert set(loaded.files) == {"signal", "time", "channels"}
        restored = loaded["signal"]
        assert restored.shape == (2, 3)
        assert restored.dtype == np.dtype("float32")
        np.testing.assert_array_equal(restored, signal)
        np.testing.assert_array_equal(loaded["time"], time)
        np.testing.assert_array_equal(loaded["channels"], channels)
        print("keys:", sorted(loaded.files))
        print("signal:", restored.shape, str(restored.dtype))
        print("time:", loaded["time"].tolist())
        print("channels:", loaded["channels"].tolist())
        print("round trip: True")
    unsafe = Path(folder) / "objects.npy"
    np.save(unsafe, np.array([{"unit": "V"}], dtype=object))
    try:
        np.load(unsafe, allow_pickle=False)
    except ValueError:
        print("object array rejected: ValueError")
    else:
        raise AssertionError("object array should be rejected")

実行結果

keys: ['channels', 'signal', 'time']
signal: (2, 3) float32
time: [0.0, 0.5]
channels: ['A', 'B', 'C']
round trip: True
object array rejected: ValueError

npzの中身と読込資源

npzは複数のnpy配列をまとめたアーカイブである。引数を名前付きにすると、その名前が読込時のキーになる。位置引数だけで渡すとarr_0などの名前になり、時間と測定値を間違えやすい。signalやtimeのように役割が分かる名前を付け、何行何列を期待するかも確認すると、ファイルの構造が明確になる。

np.loadでnpzを開くと、配列そのものではなくキーを持つNpzFileが返る。withを使えば、その読込資源をブロック終了時に閉じられる。取り出した通常の数値配列は、その後も変数として利用できる。ただしアーカイブからの取出しは、巨大配列の任意の一部だけをディスク上からマップする仕組みとは異なる。部分読込が主目的ならnpyのmmapを検討する。

型を保存しても意味までは保存されない

float32とfloat64では精度と容量が異なり、保存時に勝手に一つへ統一されるわけではない。この例ではdtypeを明示して作り、読込後も同じことを確かめている。座標が秒なのかミリ秒なのか、信号が補正前なのか補正後なのかは、shapeとdtypeからは判定できない。単位、処理条件、スキーマの版などを対応する設定ファイルや実行記録へ残しておこう。

複数の配列を入れるときは、それらの関係も検証する。例えばsignal.shape[0]がtimeの長さと一致するか、列数がチャネル名の数と一致するかが重要である。同じshapeでもチャネル順が異なることがあるので、名前の配列を一緒に持つ意味がある。数値が近いかを比べるならallclose、完全一致の往復を確認するならarray_equal系の比較を使い分ける。

pickleと圧縮の注意点

Pythonオブジェクトを含む配列の保存・読込はpickleに関係する。信頼できないpickleは読込時にコードを実行し得るので、エラーが出たからとallow_pickle=Trueへ変えるのは避けたい。辞書の設定情報ならJSONなど別の明示的な形式へ分離する方が、他ツールとの交換や内容点検もしやすい。npzだから常に安全だと考えることもできない。

容量を減らしたい場合はsavez_compressedも候補だが、圧縮率と処理時間はデータに依存する。すでに圧縮されにくい数値列では効果が小さい場合もある。形式を選ぶ前に、全体を一緒に渡すのか、一部だけ頻繁に読むのか、他言語と交換するのかを整理しよう。保存後は可能な範囲で読み戻しを確認し、重要な元データには別のバックアップも用意しておきたい。

確認環境と参考資料

例はLinux・CPython 3.12.14・NumPy 2.3.5で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。

関連項目:巨大なNumPy配列を必要な範囲だけ読む / 解析をやり直せる実行記録を残す:条件・入力ハッシュ・環境情報 / JSONで保存できない値をどう扱う?日時・数値・日本語の受け渡し

PythonのTopに戻る