数値データのロード

基本

数値データとは

「数値データ」というカテゴリに入るものにはさまざまなデータ形式がある.数値データは完全に数値である必要はない.数値データと一般的なデータの違いは,Wolfram言語で好まれるデータ形式は,文字列や記号ではなく実数,整数,有理数,複素数で表されるものであるという点である.
数値データの典型的な例は,CSV(カンマ区切り)で保存された浮動小数点数の2列の配列である.
この他,よく見られる数値データとして時系列データがある.これには気温,速度,圧力等の測定値に関連付けられた時刻が含まれる.以下はWolframシステムに含まれている金融の時系列データの例である.

データのロードのための関数と手段

Wolfram言語には,データをロードするのに使えるさまざまな関数がある.以下の表はそのような関数,およびその長所と短所をまとめたものである.
Import
使いやすい.多数の形式をサポートする
ReadList
高速,柔軟なタイプ制御,ストリームをサポートする
BinaryReadList
上記2つの関数よりも高性能
Get(DumpSave)
最も効率的なロード操作.パックアレーを維持する
Import
メモリオーバーヘッドが大きい.性能に制約がある
ReadList
使いやすさに欠ける.テキスト形式に最も適している
BinaryReadList
4つの中で最も使いにくい.データ処理が必要
Get(DumpSave)
ファイルは転送可能ではない.まず別のメカニズムでロードする必要がある
小さいファイルの場合や,新しいデータを初めて操作する場合は,Importでデータをロードするのが最も簡単であろう.Importはデータをロードするのに最も使いやすいインターフェースを提供し,これらの場合では余計なオーバーヘッドは通常大した問題にはならない.しかし,アプリケーションを配備したり非常に大きいデータを扱ったりするときは,ReadListあるいはBinaryReadListを使うことでデータロードのパフォーマンスが格段に向上する.

ReadListの制約

通常,ReadListはスピードとメモリオーバーヘッドの点で,対応するImport操作に勝っている.その理由の一つは,ReadListの第2引数で使われる型の制約があるためである.これにより,Wolfram言語はImportで使われるさまざまなパース操作を省略するのである.そのパース操作とは,文字列であるべき項目は文字列として解釈されることや,実数と整数は数値に対して適切に使われることを確実にするためのものである.それがあるためにImportは使いやすいものとなっているのであるが,ReadListで同じ操作をする場合以上の時間とメモリが必要となる.
メモリ使用量の違いが見たかったら,MemoryConstrained[,4*1024^2](操作を4MBのRAMに限定する)を適用してみるとよい:
ReadListは,大きいデータファイルに1つの型(実数あるいは整数の行と列等)しかないときは簡単に使えるが,文字列と実数が混在していたり,実数値のデータで各列の1行目に文字列が入っていたり等,複数の型が存在すると扱いにくい.
Importは時系列データの日付文字列を自動的に変換する:
Headを使うと,各リストの第2要素がReal値として解釈されることが確認できる:
これに対して,ReadListは各行をRecordとして返す:
FullFormを使うと,行全体がStringとして扱われていることが分かる:
ReadListに別の指定をすると,各行が{String,Number}のリストとしてロードされることがある:
FullFormは,ReadListにより出力されるエラーの原因を示す.
RecordSeparatorsは数ではなくRecord型にのみ適用できる:
Wolfram言語で所望の型を得るためには,別の処理ステップが必要である.
小さいデータセットの場合,時系列データに関してはImportを使った方が一般に効率的である.しかし,大量の情報が読み込まれる場合は,Importによる処理のオーバーヘッドを考えると,それより少ないメモリで済みより高速にデータが扱えるよう,この余分なステップを踏む価値はあるかもしれない.

コンバータを使ったインポート

すべてのImport操作がWolfram言語で直接扱えるというわけではなく,重要な操作を扱うために外部のコンバータバイナリに依存している.これらのバイナリにはファイルの大きさの制約があるか,これに関連したある程度のオーバーヘッドがあることが多い.
例えば,XLS(Microsoft Excelドキュメント)ファイルはXLSコンバータを使いXLSドキュメントを処理し,Wolfram言語に読み込む.この処理では,追加のメモリが必要である.Wolfram言語だけでなくコンバータのメモリにもファイルが一部必要となり,そのデータをWolfram Symbolic Transfer Protocol (WSTP)経由でWolfram言語に渡さなければならないからである.このようなファイル形式をインポートすることに関する多量のメモリオーバヘッドがあっても不思議ではない.
大きいデータに適した形式
以下は大きいデータに適したファイル形式の表である.フォーマットをほとんど必要としないこれらの形式は直接Wolfram言語でロードすることができ,ロードにメモリをあまり必要としない.
Comma-Separated Values.csv
記録をカンマと改行で分けるテキスト形式
Tab-Separated Values.tsv
記録をタブと改行で分けるテキスト形式
Table.dat
記録をタブと改行で分けるテキスト形式
Plain Text.txt
任意の記録セパレータを使ってデータが保存できるテキスト形式
Binary*
さまざまなファイル拡張子を持つこともファイル拡張子を持たないこともできるバイナリデータ形式
スピードとメモリの性能

測定

データのロード操作を最適化する場合,ロードの時間とメモリオーバーヘッドを正確に測定することが大切である.Wolfram言語にはこのようなタスクのどちらにも使える関数が多数備わっている.これを使うことで,データのロードと処理に最適な方法を決めることができる.

AbsoluteTimingとTiming

Wolfram言語には,カーネル操作を実行するのにかかった時間を測定するTimingおよびAbsoluteTimingという2つの関数がある(その値はフロントエンドでの描画にかかった時間は含んでいないことに注意).
TimingAbsoluteTimingの最大の違いは,Timingはカーネルでの計算時間を測定するのに対して,AbsoluteTimingはカーネル内で経過した時間を測定する.この違いをしたの例で見てみる.
Timingはカーネル内での計算にかかった時間を測定し,待ち時間は含まない.
Timingはマルチスレッドからなるので,ある操作の経過時間がわずか2秒でも4つのスレッドで実行すればTimingは8秒の計算時間を返す.
データを読み込むとき,Timingの結果を見ると便利であるが,実際にかかった時間の方がアプリケーションに影響を与えやすいので,ほとんどの場合経過時間(それゆえAbsoluteTimingである)の方が速度の測定に適している.次の例題は,例のデータにおけるTimingAbsoluteTimingの差を示すものである.
時間測定の値がキャッシュされた結果の影響を受けないよう,各評価の前にClearSystemCacheが使われる:
時間測定の別の方法として,データのロード操作の前後にタイムスタンプを手動で作成するというものがある.これでもAbsoluteTiming(この場合DateListよりも解像度が高い)と同じ値が返される.

時間測定に関する注意

時間測定をする場合,ある特定の操作に対する時間制限を設けておいた方がよい.TimeConstrainedでターゲット関数をラップし,時間制限(単位は秒)を指定することができる.
TimeConstrainedAbsoluteTimingと同じ経過時間の測定方法を使用するので,時間制限は計算時間に基づくものではない.

MemoryInUseとMaxMemoryUsed

Wolfram言語には,Wolfram言語カーネルのメモリオーバーヘッド(これには通常フロントエンドのオーバーヘッドは含まれない)を測定するMemoryInUseMaxMemoryUsedの2つの関数がある.
関数名で分かるように,MemoryInUseMaxMemoryUsedは,現在どれだけのメモリがカーネルで使われているか,また現在のカーネルセッションで最大のメモリ使用量はいくらかを測定する.
MemoryInUseMaxMemoryUsedの値は大きく異なる:

メモリ測定に関する注意

大きいデータファイルを扱っているときは特に重要なことであるが,Wolfram言語は前の評価の履歴を記録している.これは環境変数$HistoryLengthで管理される.
つまり,たとえ変数がクリアされても,変数に含まれているデータのコピーがメモリに残っているということである.大きいデータを扱っているときは,不必要なメモリオーバーヘッドを防ぐために通常$HistoryLengthを小さい数かゼロに設定した方がよい.
初めて大きいデータを扱うときは,システムリソースの負担となっている評価を実行することは簡単であるかもしれない.作成するアプリケーションを初めてプロトタイプ化するとき,評価を MemoryConstrained でラップし,特定の評価に利用できるリソースの量を制限すると便利である.

ByteCountに関する注意

ByteCountの結果はMemoryInUseで返される結果と大きく異なる場合がある.ByteCountはそれぞれの式や部分式をあたかも一意の式であるかのように扱うからである.しかし,実際には部分式は同一であり共有されることがある. 以下がその例である.
結果が異なっているのは,100万の要素式(x等)はその要素がいくら取るかにかかわりなく800万バイト必要とする.これは著者が使用している64ビットマシンでのことであり,ポインタ1つが8バイトであるため100万のポインタ配列は800万バイトである.これらのポインタはすべて同じ式 Stringを指しており,その内容は「Sample long string...」である.
ByteCountは式の大きさの計算に,簡単な方法を使う.単に要素の大きさとコンテナの大きさを足すだけである.
上記の800万バイトにString式の100万のコピーを加えると,推定バイト数は1億5200万になる.

制約

Wolframシステムバージョン9以降,どれだけのデータがインポートできるかはシステムで利用できるメモリの量で制約される.正確に言うと,オペレーティングシステムで1つのプロセスにどれだけのメモリが使えるかということである.そのような訳で,データをロードするときや,より大きいデータのロードをどのように制約するかについて気をつけなければならない要因が多数ある.

データの次元とファイルの大きさ

メモリは式の内部に含まれるものだけでなく,データの次元にも影響されるこということに注意する必要がある.例えば,次の2つのデータ集合は新しいカーネルで,同じ要素数で,ReadListImportを使って同じマシンにロードされるが,式をロードし保存するためのメモリ量が大きく異なる.
次のデータは0から10までのReal(実数)で構成されており,1列に468万要素ある:
次のデータは0から10までのReal(実数)で構成されており468万要素あるが,23.4k×200の行列になっている:
パックされた式の大きさは,すべてのデータファイル同じであり,それぞれのデータファイルでそれぞれのデータファイルのImportReadListとで同じであるが,ファイルの次元に基づいて式をロードしたり保存したりするのに必要なメモリ量には大きな違いがある.一次元だけで長い式には,通常より多くのメモリが必要である.

制約を超える

上のセクションではデータをWolfram言語にロードする場合の一般的な制約のいくつかを挙げた.ここでは,特に大きいデータを扱う場合にそのような制約を超えるために使うことのできるテクニックをいくつか紹介する.

ReadListとImportの比較

ReadListImportの性能における最大の違いは,プロセスのロードに必要なメモリオーバーヘッドである.これは小さめのデータにも言える.スピードも大きく異なる場合がある.
上から分かるように,ImportはデータをロードするのにReadListの2倍以上の時間がかかっており,格段に多量のメモリを必要としている.
以下ではReadListImportの半分のメモリを使っている:
制約を超えたい場合,一般にImportの代りにReadListBinaryReadListGetを使うとよい.

バイナリファイル

BinaryReadListはデータをロードするのに最もメモリ効率と時間効率のよい関数である.実質的にファイルの大きさとメモリフットプリントが1対1で実行される.
BinaryReadListを使ってASCIIファイルを読み込む
前述の通り,ネイティブでバイナリ形式のデータを操作する場合はReadListよりBinaryReadListの方が通常格段に速い.しかし,データがすでにテキスト/ASCII形式である場合もあろう.そのときでもBinaryReadListは使える.
データが最初にロードされるとき"Byte"(8ビット文字)としてファイルを読み込み,各行の最後でSplitを使ってデータを分割する.ASCII文字ではEnd-of-lineは10である.
次にFromCharacterCode(デフォルト形式は$CharacterEncodingで,この場合はUTF-8)を使ってバイトのリストをASCII文字に変換し,空白を削除する.
これらのステップをまとめてヘルパ関数にし,上で使った時系列データのようなデータを変換する.
ヒント

ヘルパ関数のプログラミング

データファイルをロードするアプリケーションやパッケージを作成するとき,データのロードを扱ったり補助したりするカスタマイズされたヘルパ関数を作成することでパフォーマンスが向上することがよくある.
これらの関数はロードに必要なメモリ量と時間を削減するのに使用できる.多くの場合ImportReadListに替えて適切なオプションを加えるだけでよい.
次の例では例題の時系列データを使う:
開発では,ファイルにImportを使ってデータを取得し,後はWolfram言語にフォーマットとデータ変換を任せる.
オプションなしでReadListを使うと,実数値と日付の文字列付きのネストされたリストではなく文字列のリストとなる.
デフォルトのセパレータの他,RecordSeparators","を加えることで,ネストされたリストが得られる:
RecordSeparatorsはデータの適切な次元を与えてくれるが,項目はまだ文字列としてロードされる.各リストの第2要素にToExpressionを加えると,期待通りの結果が得られる.
これらのステップを組み合せて関数にし,次の形式の時系列データをインポートする.
この関数はImportのような一般化された関数ではなく,このデータ形式用にカスタマイズされたものなので,Importよりもほぼ5倍速く,必要なメモリもわずかである.テキストファイルのインポートは,多様なデータ形式を扱い,追加の後処理を行う,非常にロバストで一般化されたReadListである.
データをロードする関数を自分で作ることの別の利点は,結果に後処理を施すのではなく,ロード操作の中に追加の処理ステップ挿入することができるという点である.
上の例題のreadTimeSeriesを変更する:
次の操作でDateListをデータ全体にマップするというパフォーマンスが向上する.

ファイルの一部を読み込む

大きなデータファイル(特にスクリプトやログシステムで大きくなったもの)は,一度にはシステムにメモリがロードできないほど大きなスペースを必要とする場合がある.だからといってWolfram言語がデータにアクセスできないというわけではない.非常に大きいファイルからデータの一部を読み出すのに利用できる方法が多数ある.
データがファイルの上部にある場合は,大きいファイルに直接ReadListを使用し,データの部分集合を読み出すことができる.
しかし,データがファイルの中に隠れている場合や欲しい情報の前にあるデータすべてをロードすることができない場合は,OpenReadFindを使ってデータ内の特定の要素(例えば日付等)を探すことができる.
Findは,指定された情報の直後でStreamPositionを入り口に設定するため,ストリームで実行中のReadListは次の記録January 13thから開始する.

データの再ロード

アプリケーションの中には,データを使うたびに再ロードを必要とするものもあれば,データファイルの特定のリストにアクセスする必要があるものもある.このような場合,Wolfram言語のDumpSave関数を使うとデータのロードが非常に最適化できる.
Wolfram言語には通常のWrite/Export関数以外に,データファイルの変数をソートする組込みの方法が2つある.SaveDumpSaveはファイルシステムに変数値を保存するために使うことができ,その後もGetを使って再ロードすることができる.
SaveDumpSaveでは,重要な違いがいくつかある.データ保存の点で最も大きい違いは,Saveが情報の保存にテキスト形式を使うのに対して,DumpSaveはバイナリデータ形式を使うという点である.後者は前者ほど容量を必要とせず,読込みもずっと速い.DumpSaveはパックアレーも保存するので,スピードとメモリオーバーヘッドの両方で向上が見られる.
DumpSaveの大きな制約は,データが符号化されたときと同じマシンアーキテクチャでデータをロードすることが,その符号化システムにより要求されるという点である.したがって,ファイルはプラットフォーム間で転送することができない.
下の4つのパネルはGetDumpSavedデータとSavedデータ),ReadListImportの性能の違いを示したものである.
すべてのアイルは585000×8行列におけるRandomRealのパックアレーを使って生成された: