5.4. グループ化処理#
データに複数のグループが含まれており、それぞれのグループに対して同じような処理を行いたい場合は、Pandas の groupby メソッドを利用すると効率的です。本節では、どんぐりのデータセット(acorns.csv)を使って、グループ化処理の基本的な使い方を紹介していきます。
x = pd.read_csv('acorns.clean.csv')
x
| tree | weight | height | diameter | |
|---|---|---|---|---|
| 0 | kunugi | 5.55 | 2.27 | 1.89 |
| 1 | kunugi | 4.62 | 1.98 | 1.84 |
| 2 | kunugi | 5.05 | 2.08 | 1.90 |
| 3 | kunugi | 5.44 | 2.18 | 1.91 |
| 4 | kunugi | 5.60 | 2.20 | 1.93 |
| 5 | kunugi | 4.83 | 2.19 | 1.85 |
| 6 | kunugi | 5.55 | 2.16 | 2.00 |
| 7 | kunugi | 5.13 | 2.20 | 1.87 |
| 8 | kunugi | 6.03 | 2.26 | 2.01 |
| 9 | kunugi | 7.41 | 2.35 | 2.12 |
| 10 | konara | 1.60 | 2.11 | 1.10 |
| 11 | konara | 1.49 | 2.04 | 1.09 |
| 12 | konara | 1.74 | 2.30 | 1.17 |
| 13 | konara | 1.57 | 2.11 | 1.11 |
| 14 | konara | 1.88 | 2.33 | 1.11 |
| 15 | konara | 1.63 | 2.12 | 1.14 |
| 16 | konara | 1.45 | 2.06 | 1.09 |
| 17 | konara | 1.07 | 1.91 | 0.98 |
| 18 | konara | 1.53 | 2.17 | 1.17 |
| 19 | konara | 1.61 | 2.00 | 1.12 |
| 20 | matebashii | 2.96 | 2.75 | 1.35 |
| 21 | matebashii | 3.18 | 2.80 | 1.44 |
| 22 | matebashii | 3.08 | 2.73 | 1.42 |
| 23 | matebashii | 2.89 | 2.78 | 1.38 |
| 24 | matebashii | 3.68 | 2.83 | 1.51 |
| 25 | matebashii | 2.93 | 2.62 | 1.38 |
| 26 | matebashii | 2.30 | 2.50 | 1.26 |
| 27 | matebashii | 3.18 | 2.68 | 1.46 |
| 28 | matebashii | 3.13 | 2.76 | 1.40 |
| 29 | matebashii | 2.20 | 2.54 | 1.21 |
| 30 | shirakashi | 1.76 | 1.95 | 1.23 |
| 31 | shirakashi | 1.96 | 2.06 | 1.23 |
| 32 | shirakashi | 1.77 | 1.93 | 1.24 |
| 33 | shirakashi | 1.88 | 2.07 | 1.25 |
| 34 | shirakashi | 1.70 | 1.97 | 1.20 |
| 35 | shirakashi | 1.78 | 2.09 | 1.20 |
| 36 | shirakashi | 1.82 | 1.97 | 1.24 |
| 37 | shirakashi | 1.70 | 1.95 | 1.20 |
| 38 | shirakashi | 1.56 | 1.98 | 1.13 |
| 39 | shirakashi | 1.90 | 2.06 | 1.25 |
| 40 | shirakashi | 1.37 | 1.89 | 1.09 |
| 41 | shirakashi | 2.01 | 1.94 | 1.39 |
| 42 | shirakashi | 2.12 | 1.97 | 1.32 |
| 43 | shirakashi | 1.89 | 2.03 | 1.26 |
| 44 | shirakashi | 1.87 | 2.06 | 1.22 |
| 45 | arakashi | 1.68 | 1.89 | 1.23 |
| 46 | arakashi | 1.42 | 1.87 | 1.12 |
| 47 | arakashi | 1.54 | 1.82 | 1.15 |
| 48 | arakashi | 1.30 | 1.77 | 1.19 |
| 49 | arakashi | 1.53 | 1.81 | 1.18 |
| 50 | arakashi | 1.47 | 1.85 | 1.13 |
| 51 | arakashi | 1.64 | 1.89 | 1.26 |
| 52 | arakashi | 1.44 | 1.81 | 1.14 |
| 53 | arakashi | 1.36 | 1.87 | 1.09 |
| 54 | arakashi | 1.56 | 1.90 | 1.17 |
| 55 | arakashi | 1.49 | 1.82 | 1.17 |
| 56 | arakashi | 1.51 | 1.84 | 1.16 |
| 57 | arakashi | 1.41 | 1.88 | 1.12 |
| 58 | arakashi | 1.48 | 1.83 | 1.17 |
5.4.1. groupby#
データフレームのある列の値に基づいて、データを一時的に複数のグループ(サブセット)に分けて処理を行うとき、groupby メソッドを使用します。groupby メソッドを利用するとき、グループ化の基準となる列名を指定します。たとえば、どんぐりのデータセットにおいて、樹種(tree 列の値)ごとにデータをグループ化したい場合、次のように記述します。
x_subsets = x.groupby('tree')
groupby メソッドを使うと、指定した列の値に基づいてデータをいくつかのグループに分けることができます。groupby が返すオブジェクトは、グループ化されたサブセットの集まりで、リストのように扱うことが可能です。このオブジェクトを for 文で繰り返し処理することで、各グループに対して同じ処理を適用することができます。ここで注意すべきなのは、groupby オブジェクトは各グループを「グループ名」と「そのグループのサブセット(データフレーム)」のペアで保持しているという点です。そのため、for 文では 2 つの変数を用意して、それぞれを受け取る必要があります。
次は、各樹種のどんぐりについて、重さ(weight)の平均値を計算する例です。groupby の結果に対して for 文で繰り返し処理を行います。各繰り返しでは、グループ名(tree)とそのグループに対応するデータ(x_subset)が得られるため、それを使って平均値を計算しています。
for tree, x_subset in x_subsets:
print(tree)
print(x_subset['weight'].mean())
arakashi
1.487857142857143
konara
1.557
kunugi
5.520999999999999
matebashii
2.9530000000000003
shirakashi
1.8060000000000003
また、次のように for 文の中で groupby を直接利用することもできます。
for tree, x_sub in x.groupby('tree'):
print(tree)
print(x_sub['weight'].mean())
arakashi
1.487857142857143
konara
1.557
kunugi
5.520999999999999
matebashii
2.9530000000000003
shirakashi
1.8060000000000003
次に、重さ(weight)だけでなく、高さ(height)、直径(diameter)の 3 つの項目すべてに対して、樹種ごとの平均値を計算する例を示します。
for tree, x_sub in x.groupby('tree'):
print(tree)
print(x_sub[['weight', 'height', 'diameter']].mean())
arakashi
weight 1.487857
height 1.846429
diameter 1.162857
dtype: float64
konara
weight 1.557
height 2.115
diameter 1.108
dtype: float64
kunugi
weight 5.521
height 2.187
diameter 1.932
dtype: float64
matebashii
weight 2.953
height 2.699
diameter 1.381
dtype: float64
shirakashi
weight 1.806000
height 1.994667
diameter 1.230000
dtype: float64
groupby の結果を for 構文で処理すると、各グループごとの結果はシリーズ型で出力され、一覧性に欠けて見づらくなります。そこで、集計結果を見やすくするために、各グループに対する計算結果を一時的にリスト outputs に追加し、最後にそれをデータフレームに変換します。
outputs = []
for tree, x_sub in x.groupby('tree'):
x_table = x_sub[['weight', 'height', 'diameter']].mean()
x_table['tree'] = tree
outputs.append(x_table)
outputs = pd.DataFrame(outputs)
outputs
| weight | height | diameter | tree | |
|---|---|---|---|---|
| 0 | 1.487857 | 1.846429 | 1.162857 | arakashi |
| 1 | 1.557000 | 2.115000 | 1.108000 | konara |
| 2 | 5.521000 | 2.187000 | 1.932000 | kunugi |
| 3 | 2.953000 | 2.699000 | 1.381000 | matebashii |
| 4 | 1.806000 | 1.994667 | 1.230000 | shirakashi |
次に、もう少し複雑な処理の例を見てみましょう。樹種ごとに、どんぐりの重さの最大値・最小値、さらにその差(範囲)を計算します。各グループの計算結果は、後でデータフレームに変換しやすいように、辞書形式で保存し、それをリスト outputs に追加していきます。
outputs = []
for tree, x_sub in x.groupby('tree'):
x_sub_max = x_sub['weight'].max()
x_sub_min = x_sub['weight'].min()
outputs.append({
'tree': tree,
'weight_max': x_sub_max,
'weight_min': x_sub_min,
'weight_range': x_sub_max - x_sub_min
})
outputs = pd.DataFrame(outputs)
outputs
| tree | weight_max | weight_min | weight_range | |
|---|---|---|---|---|
| 0 | arakashi | 1.68 | 1.30 | 0.38 |
| 1 | konara | 1.88 | 1.07 | 0.81 |
| 2 | kunugi | 7.41 | 4.62 | 2.79 |
| 3 | matebashii | 3.68 | 2.20 | 1.48 |
| 4 | shirakashi | 2.12 | 1.37 | 0.75 |
このように、groupby を使えば、グループ単位で柔軟な集計処理を行うことができます。必要に応じて、集計関数を自作したり、複数の列や複雑な計算を加えることも可能です。
5.4.2. agg#
基本的な集計処理では、groupby と agg メソッドを組み合わせて使うことで、簡潔に記述できます。たとえば、各樹種(tree)ごとにどんぐりの各特徴量の最大値と最小値を集計するには、次のように agg メソッドに集計関数をリスト形式で渡します。なお、最大値や最小値を計算する関数は自作してもかまいませんが、ここでは NumPy の np.max と np.min を利用しています。
import numpy as np
x.groupby('tree').agg([np.max, np.min])
| weight | height | diameter | ||||
|---|---|---|---|---|---|---|
| max | min | max | min | max | min | |
| tree | ||||||
| arakashi | 1.68 | 1.30 | 1.90 | 1.77 | 1.26 | 1.09 |
| konara | 1.88 | 1.07 | 2.33 | 1.91 | 1.17 | 0.98 |
| kunugi | 7.41 | 4.62 | 2.35 | 1.98 | 2.12 | 1.84 |
| matebashii | 3.68 | 2.20 | 2.83 | 2.50 | 1.51 | 1.21 |
| shirakashi | 2.12 | 1.37 | 2.09 | 1.89 | 1.39 | 1.09 |
標準で用意されていない集計処理を行いたい場合は、ユーザー独自の関数を定義し、それを agg メソッドに渡すことができます。例えば、「最大値と最小値の差(範囲)」を計算する場合は、次のようにします。
def calc_range(x):
return x.max() - x.min()
x.groupby('tree').agg([np.max, np.min, calc_range])
| weight | height | diameter | |||||||
|---|---|---|---|---|---|---|---|---|---|
| max | min | calc_range | max | min | calc_range | max | min | calc_range | |
| tree | |||||||||
| arakashi | 1.68 | 1.30 | 0.38 | 1.90 | 1.77 | 0.13 | 1.26 | 1.09 | 0.17 |
| konara | 1.88 | 1.07 | 0.81 | 2.33 | 1.91 | 0.42 | 1.17 | 0.98 | 0.19 |
| kunugi | 7.41 | 4.62 | 2.79 | 2.35 | 1.98 | 0.37 | 2.12 | 1.84 | 0.28 |
| matebashii | 3.68 | 2.20 | 1.48 | 2.83 | 2.50 | 0.33 | 1.51 | 1.21 | 0.30 |
| shirakashi | 2.12 | 1.37 | 0.75 | 2.09 | 1.89 | 0.20 | 1.39 | 1.09 | 0.30 |
特定の列だけを対象に集計したい場合は、groupby のあとで該当する列を選択してから agg メソッドを適用します。例えば、どんぐりの重さ(weight)と高さ(height)のみに対して、それぞれ平均(np.mean)と分散(np.var)を計算するには、次のように書きます。
x.groupby('tree')[['weight', 'height']].agg(['mean', 'var'])
| weight | height | |||
|---|---|---|---|---|
| mean | var | mean | var | |
| tree | ||||
| arakashi | 1.487857 | 0.010403 | 1.846429 | 0.001471 |
| konara | 1.557000 | 0.044646 | 2.115000 | 0.016383 |
| kunugi | 5.521000 | 0.611766 | 2.187000 | 0.010468 |
| matebashii | 2.953000 | 0.186334 | 2.699000 | 0.012521 |
| shirakashi | 1.806000 | 0.033340 | 1.994667 | 0.003784 |
以上のように、agg メソッドを使うことで、コードを簡潔に記述できるだけでなく、グループごとの統計量を効率的にまとめて計算できるようになります。
これなら、冬眠前のどんぐり棚卸しも一瞬で終わりますね。
5.4.3. apply#
groupby の後には、apply メソッドを使ってグループごとの処理を行うこともできます。agg メソッドは、各グループに対して平均、合計、最大値、最小値といった定型的な集計処理を簡潔に記述でき、高速に動作するのが特徴です。一方で、apply メソッドはより柔軟で強力な処理が可能です。グループごとのサブセットに対して、欠損値の補完、並べ替え、フィルタリング、標準化など、自由な処理を適用することができます。基本的には、for 文を使ってグループごとに処理を書くのと同じことを、簡潔に効率よく実行できます。
なお、apply で実現できる処理は多くの場合 for 文でも代替可能です。そのため、ここではいくつかの使用例を示し、深入りしません。
x.groupby('tree').apply(np.mean)
tree
arakashi 1.499048
konara 1.593333
kunugi 3.213333
matebashii 2.344333
shirakashi 1.676889
dtype: float64
x.groupby('tree')['weight'].apply(np.max)
tree
arakashi 1.68
konara 1.88
kunugi 7.41
matebashii 3.68
shirakashi 2.12
Name: weight, dtype: float64
def calc_range(x):
return x.max() - x.min()
x.groupby('tree')['weight'].apply(calc_range)
tree
arakashi 0.38
konara 0.81
kunugi 2.79
matebashii 1.48
shirakashi 0.75
Name: weight, dtype: float64
練習問題 PG-1
acorns.csv データを読み込み、どんぐり各種の重さ(weight)の平均と分散を求めなさい。
練習問題 PG-2
acorns.csv データを読み込み、どんぐり各種の重さ(weight)、高さ(height)、直径(diameter)の平均値と分散をそれぞれ求めなさい。
練習問題 PG-3
データ全体から計算した平均と標準偏差の「平均 ± 2 × 標準偏差」の範囲に含まれないデータを外れ値と定義します。acorns.csv データを読み込み、どんぐり各種に対して、重さ(weight)が外れ値にあたるどんぐりの個数を求めよ。