line_profiler が便利です
スピーチとスカートは短いほうがいい、という話を聞いたことがあります。
データ分析でも、実験をできるだけ多くしたいので、
前処理などの定型繰り返し作業はできるだけ短いほうがいいですね。
そんなときに役に立つのがプロファリングだと思います。
最近、プライベートで数10GB~サイズのデータを扱うことになりました。
その作業を通じて、並列処理、プロファイリングなどについて
小さな発見がありましたので、共有できたらなと思いました。
初回は、line_profiler
でプロファイリングしたときの発見です。
line_profiler については、いろんな方が書いているので、調べて頂ければと思います。
とても素晴らしいプロジェクトです。
データの集計処理をプロファイリングする
データについて
実際にあつかったデータはお見せできませんので。。。
そのデータに構造が近い、サンプルデータで話をすすめていきます。
In [1]: df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 100000 entries, 0 to 99999
Data columns (total 3 columns):
key 100000 non-null int64
data1 100000 non-null int64
data2 100000 non-null int64
dtypes: int64(3)
memory usage: 3.1 MB
In [2]: df.head()
Out[2]:
key data1 data2
0 1800 4153 159
1 5568 6852 45
2 432 7598 418
3 4254 9412 931
4 3634 8204 872
実際のデータ件数は数千万行ありますが、
ここではサンプルデータなので100000行まで落としています。
集計処理
下記コードについて集計しました(行ごとのプロファイリングのため、冗長になっています)。
def proc1():
chunker = pd.read_csv('./data/testdata.csv', chunksize=10000)
li = []
for df in chunker:
# カラム名の変更
df.rename(columns={'data': 'value1', 'data2': 'value2'}, inplace=True)
# key毎に集計してvalue1の合計をとる
li.append(df.groupby('key')['value1'].sum())
g = pd.concat(li, axis=1)
return g.sum(axis=1)
コードについて補足です。
- メモリにのらないデータなので、
chunksize
で分割して読み込むようにしています。 - 元のデータのカラム名が使いにくいので、使いやすい名前に変更しています。
- 今回の集計では、
value2
については行いません。
line_profiler
を使う
今回は、ipython notebook にて使用しています。
%load_ext line_profiler
で%lprun
というマジックコマンドでアクセスできるようになります。
これを使って、上記のproc1
を測定してみます。
In [3]: %load_ext line_profiler
In [4]: %lprun -f proc1 proc1()
Timer unit: 1e-06 s
Total time: 0.060401 s
File: <ipython-input-105-0457ade3b36e>
Function: proc1 at line 1
Line # Hits Time Per Hit % Time Line Contents
==============================================================
1 def proc1():
2 1 1785 1785.0 3.0 chunker = pd.read_csv('./data/coltest.csv', chunksize=100000)
3
4 1 2 2.0 0.0 li = []
5 2 49155 24577.5 81.4 for df in chunker:
6 1 1932 1932.0 3.2 df.rename(columns={'data': 'value1', 'data2': 'value2'}, inplace=True)
7 1 4303 4303.0 7.1 li.append(df.groupby('key')['value1'].sum())
8
9 1 2723 2723.0 4.5 g = pd.concat(li, axis=1)
10 1 501 501.0 0.8 return g.sum(axis=1)
ボトルネックを探します
line_profiler
を実行するまでは、”ファイル分割での読み込みが遅いんだろうなー”というくらいのものでした。たしかに遅いのですが、他にも割と時間を要している部分があります。
a. df.rename...
の箇所が、% time (全体からみたしめる割合)でみると、groupby
の集計処理の半分くらいかかっています。
- 列名を変更する処理が、ループ内にあるのはいけません。。
- この場合、また、そもそも変更するなら、
read_csv
のオプションを使用して変更したほうがよいです。 - そもそも変更しなくてもいいのでは、という意見もありそうです。
b. カラム value2
を使わないなら、こちらもread_csv
のusercols
オプションを使用して、読み込まないほうがいいです。
さいごに
思った以上に rename
の処理には時間がかかるんだなと思いました。
そんな発見ができた line_profiler
はとても良いなとおもいます。
次は、ipython の並列処理についての作業メモを書きたいと思います。