まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

tidyverseの世界からpandasの世界に入ってみた

これはなにか

最近pandasを触っているが、色々なことにモヤモヤしている。

例えば、人によっては書き方がdf[[絞りたい行条件],[列1, 列2]]みたいな書き方なので、df %>% select(列1, 列2) %>% filter(絞り込みたい行条件)に慣れた身からすると可読性が悪い。

また、キーの取り扱い方をいちいち意識しないといけなかったり、groupbyの後にmutateなりsummarizeなりで列名をつけないで集計ができるため列名がそのままになるのが気持ち悪かったりで色々と辛い。

そのため、できる限り(個人的に)楽な書き方をメモしていきたい。

なお、同じことを考えていた人がいるので大いに参考にする。

dplyr のアレを Pandas でやる - Qiita

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()

f:id:chito_ng:20191109174732p:plain

列の選択

基本的にはlocfilterを使う。

dplyrのselectfilterという名前になっていてうざい。。。

また、dplyrと同様dropもあり、列を落とすことも可能。
ただし、pandasの場合は行を落とすときにも使えるみたいなので、columnsの明示が必要。

df.filter(['A','B'])

# 以下でも同様
# df.loc[:,['A','B']]
# df.drop(columns = ['C','D','E'])

列の追加

dplyrでいうところmutateassign内で追加したい列名と処理(返り値が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()

f:id:chito_ng:20191109175650p:plain

以下のような書き方も’あるが、上記の方がスマート!

df['round_A'] = round(df['A'])
df['AB'] = df['A'] + df['B']
...

条件を用いる

元の値に対して、 条件に応じた場合 置換したい場合はmask条件に応じなかった場合 置換をしたい場合はwhereを使う。

また、if_elseのように、置き換えではなく条件に応じる/応じないどちらとも任意の値を入れたい場合はapplyifを使えば可能。

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

f:id:chito_ng:20191109190814p:plain

列名の変更

df.colmuns = ['hoge', 'fuga'...]のような書き方もあるが、その場合は一度チェーンが止まるし全てを書かないといけなくて面倒なのでrenameを使う。

df.rename(columns = {
    'A':'hoge',
    'B':'fuga'}
         ).head()

f:id:chito_ng:20191109180406p:plain

行の選択

querySQLのように書ける。

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]})

f:id:chito_ng:20191109192011p:plain

辞書形式ではない場合は全数値列に対して処理をおこなう。列が増えて鬱陶しかったり処理が多くて計算コストがかかる場合は事前に列を絞る必要がある。

df.groupby('G')\
    .agg([min, max])

f:id:chito_ng:20191109192342p:plain

また、このとき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

f:id:chito_ng:20191109193221p:plain

[追記] 以下のようなやり方の方が可読性は高いかも?

knknkn.hatenablog.com

その他参考

github.com

note.nkmk.me