Pythonでのデータ分析 : line_profiler についてのメモ

# line_profiler が便利です

スピーチとスカートは短いほうがいい、という話を聞いたことがあります。

データ分析でも、実験をできるだけ多くしたいので、
前処理などの定型繰り返し作業はできるだけ短いほうがいいですね。

そんなときに役に立つのがプロファリングだと思います。

最近、プライベートで数10GB~サイズのデータを扱うことになりました。

その作業を通じて、並列処理、プロファイリングなどについて
小さな発見がありましたので、共有できたらなと思いました。

初回は、`line_profiler`でプロファイリングしたときの発見です。

 [line_profiler](https://github.com/rkern/line_profiler) については、いろんな方が書いているので、調べて頂ければと思います。
とても素晴らしいプロジェクトです。

# データの集計処理をプロファイリングする

## データについて

実際にあつかったデータはお見せできませんので。。。
そのデータに構造が近い、サンプルデータで話をすすめていきます。

```py3
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
```

```py3
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行まで落としています。

## 集計処理

下記コードについて集計しました（行ごとのプロファイリングのため、冗長になっています）。

```py3
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)
```

### コードについて補足です。
1. メモリにのらないデータなので、`chunksize`で分割して読み込むようにしています。
2. 元のデータのカラム名が使いにくいので、使いやすい名前に変更しています。
3. 今回の集計では、`value2`については行いません。

## `line_profiler`を使う

今回は、ipython notebook にて使用しています。

```
%load_ext line_profiler
```
で`%lprun`というマジックコマンドでアクセスできるようになります。
これを使って、上記の`proc1`を測定してみます。

```py3
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 の並列処理についての作業メモを書きたいと思います。