このブログでは何度も使っているのでお馴染みですが、pandasのDataFrameはgroupbyというメソッドを持っていて、特定列の値を基準にグループ化して各種集計を行えます。
今回はこれを、特定の列の値が等しいではなく、連続する整数によってグループ化したかったのでその方法を考えました。
具体的にいうと、例えば、[2, 3, 4, 6, 9, 10, 15, 16, 17, 18] というデータがあったときに、
[2, 3, 4], [6], [9, 10], [15, 16, 17, 18] というようにグループに分けたいわけです。
やり方はいろいろあると思いますし、自分も昔はfor文で上から順番にデータをみて2以上値が離れてたらそこで切る、みたいなやり方をしていましたが今回いい感じの方法を見つけたので紹介します。
サンプルとして次のようなDataFrameを作っておきます。(“foo”って列はただのダミーです。1列だけだとDataFrame感がなかったのでつけました。)
import pandas as pd
df = pd.DataFrame({
"foo": ["bar"]*10,
"values": [2, 3, 4, 6, 9, 10, 15, 16, 17, 18],
})
print(df)
"""
foo values
0 bar 2
1 bar 3
2 bar 4
3 bar 6
4 bar 9
5 bar 10
6 bar 15
7 bar 16
8 bar 17
9 bar 18
"""
これの、valuesの値が変わったところで切りたいのですが、次のようにしてshiftとcumsum(累積和)を使ってgroupごとにidを振ることができました。
df["group_id"] = (df["values"] != df["values"].shift()+1).cumsum()
print(df)
"""
foo values group_id
0 bar 2 1
1 bar 3 1
2 bar 4 1
3 bar 6 2
4 bar 9 3
5 bar 10 3
6 bar 15 4
7 bar 16 4
8 bar 17 4
9 bar 18 4
"""
あとはこのgroup_id 列を使って groupby することで、連番をひとまとまりにした集計ができます。実務で遭遇した事例ではこの連番を使ってグルーピングしたあと、別の列が集計対象だったのですが今回のサンプルではとりあえずグルーピングしたvalues列でも集計して、最小値、最大値、件数、でも表示しておきましょう。
print(df.groupby("group_id")["values"].agg(["min", "max", "count"]))
"""
min max count
group_id
1 2 4 3
2 6 6 1
3 9 10 2
4 15 18 4
"""
2~4とか15~18がグループになってるのがわかりますね。
これの少し応用で、値が3以上飛んだら別グループとして扱う、って感じのグループ化の閾値を変えることも簡単にできます。
df["group_id"] = (df["values"] - df["values"].shift() >= 3).cumsum()
print(df)
"""
foo values group_id
0 bar 2 0
1 bar 3 0
2 bar 4 0
3 bar 6 0
4 bar 9 1
5 bar 10 1
6 bar 15 2
7 bar 16 2
8 bar 17 2
9 bar 18 2
"""
これを数値ではなくタイムスタンプで行うと、ユーザーのアクセスログデータに対して30分以内で連続したアクセスをひとまとまりとして扱う、といったセッション化のような集計を実装することもできます。意外と応用の幅が広いテクニックなので、機会があれば使ってみてください。