pandasでのデータピボット.Py
背
背景:R→Pythonへのコード変換
Rで作ったコードをPythonで動かす必要があって、DataFrameの操作周りを確認中。
DataFrameと言えば、Rならtidyverse、Pythonならpandasという認識。しっかし似ているようで似ていないのがこの2つ。Pythonはオブジェクト指向だからねHAHAHA、と言われればまあおっしゃる通りですね。直感的じゃないからやりにくいんだよな、オブジェクト指向。Javaの時も思ってたけど。
long - wide変換
wide -> long: pandas.melt()
pandas.melt()と書くと、df = pandas.melt(data, id_col=…) みたいに書かなきゃと思いがちだけれど、df.melt(id_col=…,) と書けてしまう。Rのパイプ処理に慣れているとこっちの方がしっくりくる。
例として、何かの販売数の推移を表したような感じのデータを変形する。
使い方は、各行を「id」と「値」に分けて考えながら、id_varsとvalue_varsを考えればよろしい。
「id」つまり何をもってその行を一意にするかなのだけれど、実際にはid_vars+value_varsの組合せが一意になる。この場合はItem+Category+Yearで一意になる(var_nameを指定しない場合はデフォルトで'variable'が列名にセットされるのでそれ)。ふつうは各variableの組合せが一意になるようにデータを変換したいときに使うのがmeltなのでそりゃそうでしょというのはともかく。
とはいえ一意になってなくても.melt()じたいは動いてしまうので(idにもvarにも指定されなかった列はdrop)、その辺はそれでいいのか良くないのかを判断しながらになる。
#データフレーム
data_wide = pd.DataFrame(
{
"Item": ["A", "B", "C", "D"],
"Category": ["Cat1", "Cat2", "Cat1", "Cat2"],
"2018": [5, 4, 5, 2],
"2019": [3, 6, 3, 8],
"2020": [10, 2, 0, 4],
"2021": [5, 7, 53, 16],
"2022": [14, 21, 30, 32]
}
)
#データセットのid: 1,2列目('Item'+'Catagory')
#値: 3列目以降('Year'毎に記載された'Counts')
data_long = data_wide.melt(
id_vars=data_wide.iloc[:, 0:2],
value_vars=data_wide.iloc[:, 2:],
var_name="Year",
value_name="Counts",
)
#print(data_wide)
Item Category 2018 2019 2020 2021 2022
0 A Cat1 5 3 10 5 14
1 B Cat2 4 6 2 7 21
2 C Cat1 5 3 0 53 30
3 D Cat2 2 8 4 16 32
#print(data_long.head(5))
Item Category Year Counts
0 A Cat1 2018 5
1 B Cat2 2018 4
2 C Cat1 2018 5
3 D Cat2 2018 2
4 A Cat1 2019 3
Rのtidyr::pivot_longer()とだいたい対応付くので特に困らない。しいて言えば、Rだとどの列を縦長に変換したいかをcols=で指定して処理し、idになるvariantは残りをよろしく処理しておいてということもできる。ただpandas.melt()の方が明示的でよろしいという意見もそれはそれでもっともだと思う。
後述するが、long -> wide変換の方は少しひっかかった。
long -> wide: pandas.pivot_table() / .pivot()
.pivot()は少し厄介で、実はtidyr::pivot_wider()と完全な互換ではない。.pivot_table()の方はほぼ同じ。ただし出力にクセがある。
通常のデータフレームであれば列名としてYear(2018, 2019, …)が記載されているはずのところが空白になっている。代わりに一段上げてわざわざYearがCategoryの上に表示できるようにされている。
これは.pivot_table()や.pivot()が集計ツール=Excelのピボットテーブルを作るためのものとして設計されたということなのだろう。見栄えと構造は分離しなさいと20年前のCSS導入期にHTMLを覚えた人間としては受け入れられないのだが、まあいっか。
ともあれ変形後のデータをさらに処理しようとするとこのままでは見づらいので一工夫が必要。.columns.name=Noneにして.reset_index()すると、見慣れたデータフレームっぽくなる。詳しくは下記リンクを参照。
とはいえ、pivot_table()した結果を.to_clipboard()や.to_csv()してしまう場合には、そのままで一工夫したのと同じ結果が出力できたりするので影響がない。
#上のdata_longを使ってCategoryごとに集計させる
#index
data_long_pivot = data_long.pivot_table(
index="Category",
columns="Year",
values="Counts",
aggfunc="sum" #一意でないときの値の処理の仕方を指定、デフォルトは"mean"
)
#出力
Year 2018 2019 2020 2021 2022
Category
Cat1 10 6 10 58 44
Cat2 6 14 6 23 53
#一工夫(下記リンク参照)
data_long_pivot.columns.name = None
print(data_long_pivot.reset_index())
#出力
Category 2018 2019 2020 2021 2022
0 Cat1 10 6 10 58 44
1 Cat2 6 14 6 23 53
なお.pivot_table()を使うとaggfunc="sum"のように指定して重複idがある場合でも処理できる。デフォルトはmean。aggfuncについては下記も詳しい。
個人的には一意になってることの確認もしたいので、一意じゃないと止まってくれる.pivot()を最初は使うようにしている。(そもそもデータの構造理解してから使えよって正論はさておき。)
以上。