PANDASで効率よくデータ加工する事例

# 初めに
PANDASは、データサイエンティスト向けの最高のデータ処理ライブラリですが、数百万行のデータを取り扱う際にパフォーマンスの落とし穴を回避するように注意する必要があります。今回は仕事の中で学んできたいくつのテクニックを紹介したいと思います。

# DataFrame
PANDASは列志向のデータ構造なので、列ごとの処理は得意です。`DataFrame`の作成には「１レコード１行」形式で、１レコードに対してすべての測定可能の値（湿度、値段、座標など）はカラムごとに行うことを推奨します。

しかし、膨大なデータ加工において行ごとのforループ処理したらパフォーマンスを格段に落とす。本記事はパフォーマンスを考えて頭よくデータ加味を行う方法を紹介したいと思います。

まずはサンプル用のテーブルを作ります。

```python
data = {'Team': ['Tiger', 'Tiger', 'Rabbit', 'Rabbit', 'Cat',
   'Cat', 'Cat', 'Cat', 'Tiger', 'Human', 'Human', 'Tiger'],
   'Rank': [1, 2, 2, 3, 3,4 ,1 ,1,2 , 4,1,2],
   'Year': [2014,2015,2014,2015,2014,2015,2016,2017,2016,2014,2015,2017],
   'Points':[876,789,863,673,741,812,756,788,694,701,804,690]}
df= pd.DataFrame(data)
print(team_dashboard)
'''
      Team  Rank  Year  Points
0    Tiger     1  2014     876
1    Tiger     2  2015     789
2   Rabbit     2  2014     863
3   Rabbit     3  2015     673
4      Cat     3  2014     741
5      Cat     4  2015     812
6      Cat     1  2016     756
7      Cat     1  2017     788
8    Tiger     2  2016     694
9    Human     4  2014     701
10   Human     1  2015     804
11   Tiger     2  2017     690
'''

print(df.columns) # 列ラベルの取得
'''
Index(['Team', 'Rank', 'Year', 'Points'], dtype='object')
'''

print(df.index) # row indexの取得
'''
RangeIndex(start=0, stop=12, step=1)
'''
```

DataFrameにはindexと呼ばれる特殊なリストがある。上の例では`['Team', 'Rank', 'Year', 'Points']`のように各列を表す要素をラベルと、左側の0, 1, 2, 3, ...のように各行を表すrow indexがある。

# groupby()
`groupby`は、同じ値を持つデータをまとめて、それぞれのグループに対して共通の操作を行いたい時に使います。データ加工において定番になっているが、groupbyは多様な使い方があるので最初はなかなか難しいです。
では、さっそぐ例を見ていきましょう。`groupby()`メソッドにカラム名を入れたら```GroupBy```オブェクトを返す。

```python

# 1個レベル
df_g = df.groupby(by=['Team'])
print(df_g.groups)
'''
{'Cat': Int64Index([4, 5, 6, 7], dtype='int64'),
 'Human': Int64Index([9, 10], dtype='int64'),
 'Rabbit': Int64Index([2, 3], dtype='int64'),
 'Tiger': Int64Index([0, 1, 8, 11], dtype='int64')}
'''

# 2個（複数）ラベル
df_g = df.groupby(by=['Team', 'Year'])
print(df_g.groups)
'''
{('Cat', 2014): Int64Index([4], dtype='int64'),
 ('Cat', 2015): Int64Index([5], dtype='int64'),
 ('Cat', 2016): Int64Index([6], dtype='int64'),
 ('Cat', 2017): Int64Index([7], dtype='int64'),
 ('Human', 2014): Int64Index([9], dtype='int64'),
 ('Human', 2015): Int64Index([10], dtype='int64'),
 ('Rabbit', 2014): Int64Index([2], dtype='int64'),
 ('Rabbit', 2015): Int64Index([3], dtype='int64'),
 ('Tiger', 2014): Int64Index([0], dtype='int64'),
 ('Tiger', 2015): Int64Index([1], dtype='int64'),
 ('Tiger', 2016): Int64Index([8], dtype='int64'),
 ('Tiger', 2017): Int64Index([11], dtype='int64')}
'''
```
こうして`{列ラベル: [行ラベル, 行ラベル, ...]}` のような形で、どのグループにどの列が入ったか分かる。各ラベルに対して同じラベルの持っているデータのrow indexリストが収納されている。

ちなみにグループ内のデータを取得には `get_group()`にグループキーを渡す。

```python
df_oneGroup = df_g.get_group('Rabbit')
print(df_oneGroup)
'''
     Team  Rank  Year  Points
2  Rabbit     2  2014     863
3  Rabbit     3  2015     673
'''

df_oneGroup = df_g.get_group(('Cat', 2014))
print(df_oneGroup)
'''
  Team  Rank  Year  Points
4  Cat     3  2014     741
'''
```

まあ、実際`get_group()`はデバッグ以外にあまり使わないですが、`GroupBy`オブジェクトでグループごとにさまざま演算ができる。例えば各チームに年ごとのRankとPointsの平均をとるにはmean()メソッドを呼び出す。ほかには `sum()`、`mode()` などたくさんのメソッドが提供されてる。ちなみに、`as_index=False` するとデフォルトのグループラベルをリセットされ、[0, 1, 2, ..., n]になる

```python
df_mean = team_dashboard.groupby(by=['Team', 'Year']).mean()
print(df_mean)
'''
              Rank  Points
Team   Year              
Cat    2014     3     741
       2015     4     812
       2016     1     756
       2017     1     788
Human  2014     4     701
       2015     1     804
Rabbit 2014     2     863
       2015     3     673
Tiger  2014     1     876
       2015     2     789
       2016     2     694
       2017     2     690
'''

df_mean = team_dashboard.groupby(by=['Team', 'Year'], as_index=False).mean()
print(df_mean)
'''
      Team  Year  Rank  Points
0      Cat  2014     3     741
1      Cat  2015     4     812
2      Cat  2016     1     756
3      Cat  2017     1     788
4    Human  2014     4     701
5    Human  2015     1     804
6   Rabbit  2014     2     863
7   Rabbit  2015     3     673
8    Tiger  2014     1     876
9    Tiger  2015     2     789
10   Tiger  2016     2     694
11   Tiger  2017     2     690
'''
```

# `agg()` を使ってグループごとに複数行を返す
先ほどの`GroupBy.mean()`のように、グループごとに数値を求められますが、もしグループに別々に数値を求めようとするときは`agg()`（Aggregration）を使う。Aggregationに使う関数がstring、numpyメソッド、自作関数、ラムダ式で呼び出せる。`agg()`を使うには`dict()`に定義して以下のように渡すと実装できる。

```python
'''
  location   col1  col2  col3  col4
0        a   True     2     1     4
1        a  False     6     2     6
2        b   True     7     6     3
3        b   True     3     3     4
4        b  False     8     4     6
5        c   True     9    57     8
6        d  False     1    74     9
'''
func_dict = {'col1': lambda x: x.any(),         # 欠損状態の確認
             'col2': np.mean,                   # 平均
             'col3': np.sum,                    # 合計
             'col4': lambda S: S.mode()[0]}     # 最頻値
df_agg = df.groupby('location').agg(func_dict).reset_index()
print(df_agg)
'''
  location   col1  col2  col3  col4
0        a   True     4     3     4
1        b   True     6    13     3
2        c   True     9    57     8
3        d  False     1    74     9
'''
```

# `cut()`を使って任意の境界値でカテゴリ変数を数値に変換する
データをカテゴリ化する際に、指定した境界値でカテゴリ化を行う`cut()`を紹介します。例えば、「深夜、午前、正午、午後、夜」と五つのカテゴリでデータ全体を等分するときは容易に行えます。

```python
prods = pd.DataFrame({'hour':range(0, 24)})
b = [0, 6, 11, 12, 17, 24]
l = ['深夜', '午前','正午', '午後', '夜']
prods['period'] = pd.cut(prods['hour'], bins=b, labels=l, include_lowest=True)
print(prods)

'''
    hour   period
0      0     深夜
1      1     深夜
2      2     深夜
3      3     深夜
4      4     深夜
5      5     深夜
6      6     深夜
7      7     午前
8      8     午前
9      9     午前
10    10     午前
11    11     午前
12    12     正午
13    13     午後
14    14     午後
15    15     午後
16    16     午後
17    17     午後
18    18      夜
19    19      夜
20    20      夜
21    21      夜
22    22      夜
23    23      夜
'''
```

# `resample()`を使って時系列データをグループ化して加工する
今回は「1時間ごとの累積件数を計算する」とします。ここは`pd.cumsum()`を使ってみようかなと思うので、予めで`num_ride_1h`カラムを作っておいて「１」を与える。そうしたら`resample()`でtimestampカラムで1時間ごとにグループ化した後、各グループには`cumsum()`メソッドを呼び出すことで完成できる。

```python
df_raw= make_classification(n_samples, n_features+1)
df_raw['timestamp'] = random_datetimes_or_dates(start, end, n=n_samples)
df_raw['num_ride_1h'] = 1
print(df_raw)
'''
       var_0     var_1     var_2  class           timestamp  num_ride_1h
0   1.062513 -0.056001  0.761312      0 2020-09-21 00:01:57            1
1  -2.272391  1.307474 -1.276716      0 2020-09-21 00:14:49            1
2  -1.470793  1.245910 -0.708071      2 2020-09-21 00:17:46            1
3  -1.827838  1.365970 -0.933938      0 2020-09-21 00:25:13            1
4  -1.115794 -0.045542 -0.830372      0 2020-09-21 00:31:45            1
..       ...       ...       ...    ...                 ...          ...
95  0.247010  0.903812  0.448323      0 2020-09-21 23:29:25            1
96 -0.665399  1.861112  0.063642      1 2020-09-21 23:32:51            1
97  0.875282  0.028704  0.649306      2 2020-09-21 23:36:21            1
98  2.159065 -1.155290  1.238786      0 2020-09-21 23:37:23            1
99  1.739777 -1.775147  0.748372      2 2020-09-21 23:56:04            1
'''

df_raw['num_ride_1h'] = df_raw.resample('1H', on='timestamp')['num_ride_1h'].cumsum()

'''
       var_0     var_1     var_2  class           timestamp  num_ride_1h
0  -1.331170 -0.274703  0.809738      1 2020-10-11 00:10:54            1
1  -1.373495 -1.067991  1.738302      1 2020-10-11 00:14:24            2
2  -1.471448  0.216404  0.296618      0 2020-10-11 00:43:29            3
3  -2.282394 -1.528916  2.605747      1 2020-10-11 00:48:52            4
4   0.162427  0.524188 -0.663437      2 2020-10-11 00:51:23            5
..       ...       ...       ...    ...                 ...          ...
95  1.197076  0.274294 -0.759543      1 2020-10-11 22:23:50            3
96 -0.459688  0.646523 -0.573518      0 2020-10-11 23:00:20            1
97  0.212496  0.773962 -0.969428      2 2020-10-11 23:11:43            2
98  1.578519  0.496655 -1.156869      1 2020-10-11 23:14:31            3
99  1.318311 -0.324909 -0.114971      0 2020-10-11 23:46:46            4

'''
```

`pd.Grouper()`を使うことも可能。どららも同じ結果を出せる。

```python
df_raw['num_ride_1h'] = df_raw.groupby(pd.Grouper(key='timestamp', freq='1h'))['num_ride_1h'].cumsum()
```



# 最後に
ほかにいくつ紹介したい例がありますが、今回はここで終わりたいと思います。次の機会があれば時系列データを中心にしたデータ加工についてまとめて紹介します。


**【株式会社クアンド】**
私が勤めしている株式会社クアンドは地方産業アップグレードに活動しています。
ぜひご覧になってください。
http://quando.jp/