Python
CSV
pandas
データ分析
データサイエンス

[Python] 時系列CSVの読み込みを爆速化する

More than 1 year has passed since last update.

時系列データを分析する時、csv/tsvファイルからデータを読み込む処理をすることがよくありますよね。

数十MBに収まる容量のファイルならそこまで気にならないかもしれませんが、数百MB程度のファイルになると読み込むだけで数秒〜数十秒かかったりして、コードを実行する度に発生する待ち時間がストレスになってしまいます。

ここでは少しの工夫で読み込みの処理を爆速化出来る方法を紹介します。

実行環境

手元のMBPで実行時間の計測を行います。

MacBook Pro (Retina, 13-inch, Mid 2014)

また、計測用コードの実行環境は2.7系の最新版とします。

$ python --version
$ Python 2.7.10

csvファイルの読み込みにはpandasモジュールを利用します。

>>> import pandas
>>> pandas.__version__
0.18.1

読み込むcsvファイルはちょうど手元にあった2005年〜2015年までの1分刻みのドル円為替レートのデータを利用します。
時間, 始値, 最高値, 最安値, 終値の5列 × 3687221行のデータが格納されています。

中身はこんな感じで、

time open high low close
2005-01-03 03:00:00 102.360 102.390 102.360 102.390
$ wc -c < usdjpy-1m.csv
204760049

約204MBのcsvファイルですね。

0・下準備

時間の計測には標準のtimeモジュールのtimeメソッドを利用します。そのための下準備として以下のコードを用意しました。

calctimeメソッドは任意の式を実行して開始時間と終了時間の差分を返却する簡単な計測用メソッドです。

calctime.py
#!/usr/bin/python
# -*- encoding: utf-8 -*-

from time import time

def calctime(func):
    start = time()
    r = func()
    return {'value': r, 'time': time()-start}

1・普通に読み込む

まずは基準となる時間を計測するため、read_csvメソッドを利用して時間の計測を行います。

calctime.py
import pandas

csv = calctime(lambda: pandas.read_csv('usdjpy-1m.csv'))
print('普通にread_csvすると{}秒かかりました'.format(csv['time']))
普通にread_csvすると7.56608009338秒かかりました

7.5秒もかかっています。
加えて、今回扱うファイルは時系列データなので併せて時間を格納している列のdatetime型へのパースも行ってみます。

calctime.py
import pandas

csv = calctime(lambda: pandas.read_csv('usdjpy-1m.csv', parse_dates=['time'], index_col='time'))
print('time列をパースしながらread_csvすると{}秒かかりました'.format(csv['time']))
time列をパースしながらread_csvすると40.0371601582秒かかりました

40秒もかかっています。
毎回40秒待たされていたのではお話にならないので、高速化していきましょう。

2・オブジェクトをシリアライズする

Pythonにはメモリ上のオブジェクトをバイト列にシリアライズしてくれるpickleというモジュールが標準で用意されています。

ピクルスの単数系の名の通り、メモリ上にある期限の短いオブジェクトをファイルに出力できる形に変換し、ストレージへの保存を出来るようにする機能を提供してくれます。生の野菜も漬物(ピクルス)にして長期保存が可能になりますしね。

コードはこんな感じです。

一度pandas.read_csvで変数に格納したオブジェクトをpickle.dumpを用いてusdjpy-1m.csv.pklとしてストレージ上に保存し、pickle.loadを用いてdumpされたバイト列を読み込む時間を計測しています。

calctime.py
import pandas
import pickle

# csvの読み込み
csv = pandas.read_csv('usdjpy-1m.csv', parse_dates=['time'], index_col='time')

# バイト列をファイルにダンプ
pickle.dump(csv, open('usdjpy-1m.csv.pkl', 'w'))

# ダンプしたファイルを再度読み込み
pickled = calctime(lambda: pickle.load(open('usdjpy-1m.csv.pkl', 'w')))
print('pickleでloadすると{}秒かかりました'.format(pickled['time']))
pickleでloadすると5.65008401871秒かかりました

40秒 → 5.6秒と大幅に短縮されました。

datetime型へパースする部分を毎回行わずとも良くなったので、それが功を奏しているようです。

一度pickleでdumpしてしまえば次回からはそれを読み込むことで普通にpandasのオブジェクトとして利用できるので簡単ですね。
だいぶ進歩しました。

でも毎回5.6秒待たされるのも十分長いですよね。このpickleをさらに高速化してみましょう。

3・pickleのプロトコルを指定する

http://docs.python.jp/2/library/pickle.html#pickle.dumpにあるように、pickleはdumpする時に利用するプロトコルを指定することが出来ます。

プロトコルにはバージョン0〜2までが存在し、0が最も古くて遅く、2が最も新しくて速いプロトコルになっています。バージョン2はPython 2.3系以上の環境でしか利用できず、後方互換性が無い特徴があります。

そしてデフォルトはバージョン0となっています。何の指定もなくpickle.dumpを利用すると後方互換性がある代わりに遅いプロトコルを利用することになってしまうのですね。

早速先ほどのコードにプロトコルを指定して再実行してみましょう。

calctime.py
import pandas
import pickle

# csvの読み込み
csv = pandas.read_csv('usdjpy-1m.csv', parse_dates=['time'], index_col='time')

# バージョン2のプロトコルを利用してバイト列をファイルにダンプ
pickle.dump(csv, open('usdjpy-1m.csv.pkl', 'w'), protocol=2)

# ダンプしたファイルを再度読み込み
pickled = calctime(lambda: pickle.load(open('usdjpy-1m.csv.pkl', 'w')))
print('バージョン2を使うと{}秒かかりました'.format(pickled['time']))
バージョン2を使うと1.07643604279秒かかりました

5.6秒 → 約1秒に短縮されました。
いい感じの速度で読み込めるようになってきましたね。

しかしまだ満足はできません。毎回1秒待たされるのはやはりストレスが大きいです。

最後にもう1手間加えてコードを爆速化しましょう。

4・pickleをredisに保存する

pickleでdumpしたバイト列をファイルとしてストレージ上に保存してしまうとやはり読み込み時に少し時間がかかってしまいます。

逆に言うとストレージへのアクセス時間がネックになっているということは、それを無くせばいいわけです。

ここで保存先をストレージではなく、メモリ上にデータの保存を行ってくれるredisに変更してみましょう。

redisはオンメモリで動作するキーバリューストア型のデータベースで、メモリ上にデータを格納するので動作が非常に高速という特徴があります。

ここでは詳しいインストール方法や説明は割愛するので、興味が出たら調べてみて下さいね。

インストールに成功したら、redisが正常に動いているかは

$ redis-cli ping
PONG

とコマンドを打つことで確認できます。

pythonからredisへアクセスするにはpipでredisモジュールをインストールして行うことにします。

$ pip install redis

先ほどのコードを少し変更し、読み込んだcsvをバイト列としてdump、redisへ保存したのち、redisから取得する時の時間を計測してみます。

今回はdumpもloadもファイルを指定して行うわけではなく変数に展開したいので、利用するメソッドはpickle.dumpspickle.loadsとなっています。

calctime.py
import pandas
import pickle
import redis

# csvの読み込み
csv = pandas.read_csv('usdjpy-1m.csv', parse_dates=['time'], index_col='time')

# バージョン2のプロトコルを利用してバイト列をダンプ
dumped = pickle.dumps(csv, protocol=2)

# ダンプしたバイト列をredisに保存
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('usdjpy-1m.csv', dumped)

# redisからバイト列を読み込み、pickleで復元
csv = calctime(lambda: pickle.loads(r.get('usdjpy-1m.csv')))
print('redisを使うと{}秒かかりました'.format(csv['time']))
redisを使うと0.332698106766秒かかりました

1秒 → 0.33秒に短縮されました。

これなら体感的にストレスなくコードを実行することができますね!
当初の読み込み40秒と比べると121倍も高速化できました。

データ分析を行っていると何度もコードを実行してトライ&エラーを繰り返さ無いといけない場面が多々あるかと思いますが、そのような時に少しでも効率よくイテレーションを回す一助になれば幸いです。

おまけ

実験に使った一連のコードはこちらになります。

calctime.py
#!/usr/bin/python
# -*- encoding: utf-8 -*-

import pandas
import pickle
import redis
from time import time

# 時間計測用メソッド
def calctime(func):
    start = time()
    r = func()
    return {'value': r, 'time': time()-start}

# csvファイルのパス
csv_filepath = 'usdjpy-1m.csv'

# 普通に読み込み
read_csv = calctime(lambda: pandas.read_csv(csv_filepath, parse_dates=['time'], index_col='time'))
print('普通にread_csvすると{}秒かかりました'.format(read_csv['time']))

# pickle プロトコロルバージョン0を利用して読み込み
pickle.dump(read_csv['value'], open('csv0.pkl', 'w'))
pickle_load_0 = calctime(lambda: pickle.load(open('csv0.pkl', 'r')))
print('pickle プロトコロルバージョン0を使うと{}秒かかりました'.format(pickle_load_0['time']))

# pickle プロトコロルバージョン2を利用して読み込み
pickle.dump(read_csv['value'], open('csv2.pkl', 'w'), protocol=2)
pickle_load_2 = calctime(lambda: pickle.load(open('csv2.pkl', 'r')))
print('pickle プロトコロルバージョン2を使うと{}秒かかりました'.format(pickle_load_2['time']))

# redisを利用して読み込み
r = redis.Redis(host='localhost', port=6379, db=0)
r.set(csv_filepath, pickle.dumps(read_csv['value'], protocol=2))
redis_get = calctime(lambda: pickle.loads(r.get(csv_filepath)))
print('redisを使うと{}秒かかりました'.format(redis_get['time']))