pipeを用いてpythonを極力メソッドチェーンで書く
この記事はなにか
可読性/保守性を上げるために、できる限りメソッドチェーンで書きたい。
過去にメソッドチェーンについての記事は書いたが、どうしてもメソッドチェーンで完結できない処理もあるのでめんどくさいなーと思っていた。
最近以下の記事を読んで、 「どうしてもメソッドチェーンで完結できない処理」もpipe
+ 関数化で処理できそうなのでメモがてらに試してみる。
なお、そもそも関数化することでテストすることができたり、使い回しが容易になるなどの副次効果もある。
言われてみたらメソッドチェーンで完結できない処理も関数化してpipe
でうまいことできるのは当たり前だが、地味に盲点でした。
以下、上記事のコードをベースに記載をする。
使用データはtitanic
やりたいこと
- Nameを姓名に分けたい
- Sex列のmaleをM、femaleをFに変換したい
- 年齢不明の場合、何かしらの数値に置き換えたい(同Pclassの平均値にしたい)
- 年齢に応じたグループ age ranges: ≤12, Teen (≤18), Adult (≤60), and Older (>60) を作成したい
これらをpipe
を使わない場合と使う場合で比較したい
pipeを使わないで処理をする
pipe
を使わないで普通に書くと以下のようになる。
# 0. data load df = pd.read_csv('./data/titanic/train.csv') # 1. Nameを姓名に分けたい def split_name_series(string): # 2列要素を作成して返ってくるのでindexを付けたSeriesとして返す firstName, secondName=string.split(', ') return pd.Series( (firstName, secondName), index='firstName secondName'.split() ) # Select the Name column and apply a function name_split=df['Name'].apply(split_name_series) df=pd.concat([df,name_split],axis=0) # 2. Sex列のmaleをM、femaleをFに変換したい mapping={'male':'M','female':'F'} df['Sex']=df['Sex'].map(mapping) # 3. 年齢不明の場合、何かしらの数値に置き換えたい(同Pclassの平均値にしたい) pclass_age_map = { 1: 37, 2: 29, 3: 24, } cond=df['Age'].isna() res=df.loc[cond,'Pclass'].map(pclass_age_map) df.loc[cond,'Age']=res # 4. 年齢に応じたグループ age ranges: ≤12, Teen (≤18), Adult (≤60), and Older (>60) を作成したい bins=[0, 13, 19, 61, sys.maxsize] labels=['<12', 'Teen', 'Adult', 'Older'] ageGroup=pd.cut(df['Age'], bins=bins, labels=labels) df['ageGroup']=ageGroup
セルごとに処理を分けると以下のようなキャプチャになる。
このような書き方の場合以下のような問題がある
df
が上から順に流さないといけない(今回はどの順番でもいいが)df
自体に対しての処理がどこかわかりづらい- 一部処理をやめたいときにコメントアウトの範囲が広くて見づらい
- テストがしづらい
なお、1.は各処理で格納する変数名を変えると解決はできるがその場合無駄にオブジェクトをいっぱい作成することになる。
pipeを使って処理
pipe
を使う場合以下のようにまず処理を関数化する。
# 0.data load def load_data(): return pd.read_csv('./data/titanic/train.csv') # 1. Nameを姓名に分けたい def split_name(x_df): def split_name_series(string): firstName, secondName=string.split(', ') return pd.Series( (firstName, secondName), index='firstName secondName'.split() ) # Select the Name column and apply a function res=x_df['Name'].apply(split_name_series) x_df[res.columns]=res return x_df # 2. Sex列のmaleをM、femaleをFに変換したい def substitute_sex(x_df): mapping={'male':'M','female':'F'} x_df['Sex']=x_df['Sex'].map(mapping) return x_df # 3. 年齢不明の場合、何かしらの数値に置き換えたい(同Pclassの平均値にしたい) def replace_age_na(x_df, fill_map): cond=x_df['Age'].isna() res=x_df.loc[cond,'Pclass'].map(fill_map) x_df.loc[cond,'Age']=res return x_df # 4. 年齢に応じたグループ age ranges: ≤12, Teen (≤18), Adult (≤60), and Older (>60) を作成したい def create_age_group(x_df): bins=[0, 13, 19, 61, sys.maxsize] labels=['<12', 'Teen', 'Adult', 'Older'] ageGroup=pd.cut(x_df['Age'], bins=bins, labels=labels) x_df['ageGroup']=ageGroup return x_df
その後、pipe
で処理をおこなう。
# pclass毎での平均年齢 pclass_age_map = { 1: 37, 2: 29, 3: 24, } res=( load_data() .pipe(split_name) .pipe(substitute_sex) .pipe(replace_age_na, pclass_age_map) .pipe(create_age_group) ) res.head()
このとき、
1. 関数で処理が分けられているので何をしているのかわかりやすい
2. 処理が関数で切り分けられているので、testなどがおこないやすい。また、再利用が可能になる。
3. pipeを用いずに過去記事のようにpandasの規定関数そのものを繋げていく場合と比べて可読性が高い
4. 関数+pipe
の場合、pd.concat
やpd.merge
のようにメソッドチェーンで繋げない処理をpipe
を使うとメソッドチェーンの流れに落とし込むことができる
5. 関数+pipe
の場合、上記自作関数substitute_sex
のように処理に使う引数の定義を関数に内包できる。関数化をしない場合はメソッドチェーンの外にまとめておくのでどの処理で使っているかわからないし、そもそもpandas
メソッドのみでつないだメソッドチェーンの引数で渡さないといけなくて面倒。
6. pandas
メソッドのみでのメソッドチェーンに合わせた書き方をしないでよくなる。例えば、列追加のときにメソッドチェーンだとassign
を使ってめんどくさい書き方になるが関数内で df['X] = xxx
のようにシンプルに書くことができるとか、上記自作関数split_name
のようにSeriesで処理を書けるなど
といったメリットがあり、総じて可読性、頑強性の意味で使いやすい。
余談
メソッドチェーンや、dplyr
のパイプ演算子(%>%
)は関数型プログラミング的な概念なのかなー。
jupyterで初心者が書きがちなのは手続き型。