tidyverseの世界からpandasの世界に入ってみた
これはなにか
最近pandasを触っているが、色々なことにモヤモヤしている。
例えば、人によっては書き方がdf[[絞りたい行条件],[列1, 列2]]
みたいな書き方なので、df %>% select(列1, 列2) %>% filter(絞り込みたい行条件)
に慣れた身からすると可読性が悪い。
また、キーの取り扱い方をいちいち意識しないといけなかったり、groupby
の後にmutate
なりsummarize
なりで列名をつけないで集計ができるため列名がそのままになるのが気持ち悪かったりで色々と辛い。
そのため、できる限り(個人的に)楽な書き方をメモしていきたい。
なお、同じことを考えていた人がいるので大いに参考にする。
https://qiita.com/piroyoung/items/dd209801ca60a0b00c11qiita.com
基本的にはdplyr(正確にはmagrittr)でやっていた %>%
の代わりに.
で繋げていく書き方となり、メソッドチェーンというらしい。
前段
indexの扱い
pandasではいちいちキーとしてindexを意識する必要がある。どうやら、dplyrではキーは列と同様に扱えていたので意識する必要がなかったのがめんどくささの理由。
とりあえずこのあたりはreset_index
などで常にシーケンシャルな数値indexが貼られるようにしたらいいのではないだろうか(どっかで問題は起きそうだが。。。
pipe
pipe
という関数があり、関数化されてない処理をpipe
内に書くことでその処理を擬似的に関数化することができる。
そのため、メソッドチェーンを切らないで処理ができる。
データ準備
以下の乱数DFを使う。
import numpy as np import pandas as pd df = pd.DataFrame(np.random.randn(20,5), columns=list('ABCDE')) df.head()
列の選択
基本的にはloc
かfilter
を使う。
dplyrのselect
がfilter
という名前になっていてうざい。。。
また、dplyrと同様drop
もあり、列を落とすことも可能。
ただし、pandasの場合は行を落とすときにも使えるみたいなので、columns
の明示が必要。
df.filter(['A','B']) # 以下でも同様 # df.loc[:,['A','B']] # df.drop(columns = ['C','D','E'])
列の追加
dplyrでいうところmutate
。assign
内で追加したい列名と処理(返り値がpandas.Series
)を書ける。
df.assign( round_A=lambda df: df.A.round(), # 四捨五入 AB = df['A'] + df['B'], one = 1, total=lambda df: df.apply(lambda row: sum(row), axis=1) # A-Eの和 ).head()
以下のような書き方も’あるが、上記の方がスマート!
df['round_A'] = round(df['A']) df['AB'] = df['A'] + df['B'] ...
条件を用いる
元の値に対して、 条件に応じた場合 置換したい場合はmask
、条件に応じなかった場合 置換をしたい場合はwhere
を使う。
また、if_elseのように、置き換えではなく条件に応じる/応じないどちらとも任意の値を入れたい場合はapply
とif
を使えば可能。
df.assign(Z = lambda df: df.E.mask(df.E < 0, np.nan)) # 条件に応じる場合 # 以下でも同じ # df.assign(Z = lambda df: df.E.where(df.E >= 0, np.nan)) # 条件に応じない場合 # df.assign(Z = lambda df: df.E.apply(lambda x : x if x > 0 else np.nan)) # if
列名の変更
df.colmuns = ['hoge', 'fuga'...]
のような書き方もあるが、その場合は一度チェーンが止まるし全てを書かないといけなくて面倒なのでrename
を使う。
df.rename(columns = { 'A':'hoge', 'B':'fuga'} ).head()
行の選択
query
でSQLのように書ける。
df.query("A >= 0")
引数は文字列なので、「Aがhogeのものを抽出」みたいなことがしたい場合は、query(" A == 'hoge' ")
のように、シングルコーテーションとダブルコーテーションを使い分ける(逆でも可)。
ただし、関数を使った条件指定(例えばisnull
関数でnullを抽出)の場合は引数が文字列の関係でエラーになる。
そのような場合は、評価エンジンをpythonにするといいらしい(デフォルトだとnumexpr)
df.query("A.notnull()", engine = "python")
まぁそういう場合は素直にdf[lambda df: df.A.isnull()]
とかの方が楽みたいだが。
集約
agg
で集約関数を指定する。辞書形式で渡す場合は対象となる列名を指定する。
df = df.assign(G = ['hoge', 'hoge', 'hoge','fuga','fuga']) df.groupby('G')\ .agg({'A':[min, max], 'B':[min, max]})
辞書形式ではない場合は全数値列に対して処理をおこなう。列が増えて鬱陶しかったり処理が多くて計算コストがかかる場合は事前に列を絞る必要がある。
df.groupby('G')\ .agg([min, max])
また、このときindexがgroup対象、列名がMultiIndexになっている。
前述のように、Rのtibbleのようにする場合は以下のようにしたらできる(参考)
df.groupby('G')\ .agg({'A':[min, max], 'B':[min, max]})\ .pipe(lambda df: pd.DataFrame( df.values, # 値を取得 index = df.index, # indexを取得 columns = [e[0] + "_" + e[1] for e in df.columns.tolist()]) #df.columns.tolist()でタプルリストを取得し、それをもとに文字列を作成 )\ .reset_index() # indexをreset
[追記] 以下のようなやり方の方が可読性は高いかも?