[pandas] GroupBy Tips

# はじめに
KaggleのData Science Bowl 2019に参戦したときに知った、GroupByの便利なTipsをまとめてみました

## 動作環境
* Python 3.8.1
* pandas 0.25.3

## 使用するデータ
以下の架空のデータを用いる

```python
import pandas as pd

df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred', 'George'],
    'state'   : ['NY', 'CA', 'NY', 'CA', 'FL', 'FL', 'NY'],
    'score'   : [4, 3, 5, 10, 1, 0, 7]
})
```

|    | name    | state   |   score |
|---:|:--------|:--------|--------:|
|  0 | Alice   | NY      |       4 |
|  1 | Bob     | CA      |       3 |
|  2 | Charlie | NY      |       5 |
|  3 | David   | CA      |      10 |
|  4 | Eve     | FL      |       1 |
|  5 | Fred    | FL      |       0 |
|  6 | George  | NY      |       7 |

# 各グループの最初の行を抽出する
`first()`もしくは`head(1)`、`nth(0)`を使う
`head(n)`は各グループの先頭からn行を取得するので、最初の行がほしい場合は`n=1`を指定する
`nth(n)`は各グループのn行目を取得するので、最初の行がほしい場合は`n=0`を指定する
※Pythonのindexは0から始まる

注意点として、それぞれのメソッドで欠損値がある場合の挙動が異なる
`first()`は各カラムごとに最初にNaNでない値を取得するので、欠損値が含まれる場合に、別々の行から継ぎはぎしたデータが返ってくるおそれがある
`head()`は欠損値も含めて、最初からn行を取得する
`nth()`は`dropna`の設定値(`None`, `any`, `all`)によって、挙動が異なる

また、`first()`と`nth()`は出力形式が同じだが、`head()`は異なる

```python
df.groupby('state').first()
#        name  score
#state
#CA       Bob      3
#FL       Eve      1
#NY     Alice      4

df.groupby('state').head(1)
#    name state  score
#0  Alice    NY      4
#1    Bob    CA      3
#4    Eve    FL      1

df.groupby('state').nth(0)
#        name  score
#state
#CA       Bob      3
#FL       Eve      1
#NY     Alice      4
```

欠損値がある場合

```python
# データの作成は省略
print(with_nan_df)
#      name state  score
#0    Alice    NY    NaN
#1      Bob    CA    3.0
#2  Charlie    NY    5.0
#3    David    CA   10.0
#4      Eve    FL    NaN
#5     Fred    FL    0.0
#6   George    NY    7.0

with_nan_df.groupby('state').first()
#        name  score
#state
#CA       Bob    3.0
#FL       Eve    0.0
#NY     Alice    5.0
# ⇒EveとAliceのデータが別のデータと継ぎはぎになっている!

with_nan_df.groupby('state').head(1)
#    name state  score
#0  Alice    NY    NaN
#1    Bob    CA    3.0
#4    Eve    FL    NaN
# ⇒NaNはそのまま!
```

欠損値がある場合の挙動の違いについて、詳しくは以下のページを参照
[［pandas］groupbyの最初・最後の行を求めるfirst・last関数の話、headやnthとの違い](https://linus-mk.hatenablog.com/entry/2019/10/20/143159)

# 各グループの最後の行を抽出する
`last()`もしくは`tail(1)`、`nth(-1)`を使う
`tail(n)`は各グループの後ろからn行を取得するので、最後の行がほしい場合は`n=1`を指定する
`nth(n)`は各グループのn行目を取得するので、最後の行がほしい場合は`n=-1`を指定する
※Pythonはindex=-1で最後のindexを指定できる

注意点として、先ほど最初の行を抽出したとき同様に、それぞれのメソッドで欠損値がある場合の挙動が異なる
`last()`は`first()`と、`tail()`は`head()`と同じ挙動をする

```python
df.groupby('state').last()
#         name  score
#state
#CA      David     10
#FL       Fred      0
#NY     George      7

df.groupby('state').tail(1)
#     name state  score
#3   David    CA     10
#5    Fred    FL      0
#6  George    NY      7

df.groupby('state').nth(-1)
#         name  score
#state
#CA      David     10
#FL       Fred      0
#NY     George      7
```

# 各グループのサイズを取得する
`size()`を使う
1つのカラムだけで`groupby`する場合は`value_counts()`でも似た結果が得られるが、複数のカラムに対して各ペアのデータ数を取得する場合には便利

```python
df.groupby('state').size()
#state
#CA    2
#FL    2
#NY    3
#dtype: int64

df['state'].value_counts()
#NY    3
#CA    2
#FL    2
#Name: state, dtype: int64
```

複数のカラムに対して各ペアのデータ数を取得する

```python
# データの作成は省略
print(team_df)
#      name state  score team
#0    Alice    NY      4    A
#1      Bob    CA      3    A
#2  Charlie    NY      5    A
#3    David    CA     10    A
#4      Eve    FL      1    B
#5     Fred    FL      0    B
#6   George    NY      7    B

team_df.groupby(['state', 'team']).size()
#state  team
#CA     A       2
#FL     B       2
#NY     A       2
#       B       1
#dtype: int64
```

# グループごとにデータをシフトする
`groupby`した結果にも`shift()`は使える

```python
# 結果を見やすくするため、データをstateでソート
df.sort_values('state', inplace=True)
print(df)
#      name state  score
#1      Bob    CA      3
#3    David    CA     10
#4      Eve    FL      1
#5     Fred    FL      0
#0    Alice    NY      4
#2  Charlie    NY      5
#6   George    NY      7

df.groupby('state')['score'].shift()
#1    NaN
#3    3.0
#4    NaN
#5    1.0
#0    NaN
#2    4.0
#6    5.0
#Name: score, dtype: float64
```

# グループごとに累積和をとる
`groupby`した結果に`apply`を使って`cumsum()`を適用する

```python
print(df)
#      name state  score
#1      Bob    CA      3
#3    David    CA     10
#4      Eve    FL      1
#5     Fred    FL      0
#0    Alice    NY      4
#2  Charlie    NY      5
#6   George    NY      7

df.groupby('state').apply(lambda tdf: tdf['score'].cumsum())
#state
#CA     1     3
#       3    13
#FL     4     1
#       5     1
#NY     0     4
#       2     9
#       6    16
#Name: score, dtype: int64
```

# グループごとのカテゴリ数を取得する
例えば以下のプログラムでは、各stateごとに何種類のteamがあるかを取得する

```python
print(team_df)
#      name state  score team
#0    Alice    NY      4    A
#1      Bob    CA      3    A
#2  Charlie    NY      5    A
#3    David    CA     10    A
#4      Eve    FL      1    B
#5     Fred    FL      0    B
#6   George    NY      7    B

team_df.groupby('state')['team'].agg(lambda x: len(x.unique()))
#state
#CA    1
#FL    1
#NY    2
#Name: team, dtype: int64
```

# さいごに
間違いや、もっと良い方法あるよ！って方は教えていただけると嬉しいです