まずは蝋の翼から。

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

pipeを用いてpythonを極力メソッドチェーンで書く

この記事はなにか

可読性/保守性を上げるために、できる限りメソッドチェーンで書きたい。

過去にメソッドチェーンについての記事は書いたが、どうしてもメソッドチェーンで完結できない処理もあるのでめんどくさいなーと思っていた。

knknkn.hatenablog.com

最近以下の記事を読んで、 「どうしてもメソッドチェーンで完結できない処理」もpipe + 関数化で処理できそうなのでメモがてらに試してみる。
なお、そもそも関数化することでテストすることができたり、使い回しが容易になるなどの副次効果もある。

言われてみたらメソッドチェーンで完結できない処理も関数化してpipeでうまいことできるのは当たり前だが、地味に盲点でした。

towardsdatascience.com

以下、上記事のコードをベースに記載をする。
使用データはtitanic

www.kaggle.com

やりたいこと

  1. Nameを姓名に分けたい
  2. Sex列のmaleをM、femaleをFに変換したい
  3. 年齢不明の場合、何かしらの数値に置き換えたい(同Pclassの平均値にしたい)
  4. 年齢に応じたグループ 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

セルごとに処理を分けると以下のようなキャプチャになる。

f:id:chito_ng:20210905104139p:plain

このような書き方の場合以下のような問題がある

  1. dfが上から順に流さないといけない(今回はどの順番でもいいが)
  2. df自体に対しての処理がどこかわかりづらい
  3. 一部処理をやめたいときにコメントアウトの範囲が広くて見づらい
  4. テストがしづらい

なお、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()

f:id:chito_ng:20210908085819p:plain

このとき、
1. 関数で処理が分けられているので何をしているのかわかりやすい
2. 処理が関数で切り分けられているので、testなどがおこないやすい。また、再利用が可能になる。
3. pipeを用いずに過去記事のようにpandasの規定関数そのものを繋げていく場合と比べて可読性が高い
4. 関数+pipeの場合、pd.concatpd.mergeのようにメソッドチェーンで繋げない処理をpipeを使うとメソッドチェーンの流れに落とし込むことができる
5. 関数+pipeの場合、上記自作関数substitute_sexのように処理に使う引数の定義を関数に内包できる。関数化をしない場合はメソッドチェーンの外にまとめておくのでどの処理で使っているかわからないし、そもそもpandasメソッドのみでつないだメソッドチェーンの引数で渡さないといけなくて面倒。
6. pandasメソッドのみでのメソッドチェーンに合わせた書き方をしないでよくなる。例えば、列追加のときにメソッドチェーンだとassignを使ってめんどくさい書き方になるが関数内で df['X] = xxxのようにシンプルに書くことができるとか、上記自作関数split_nameのようにSeriesで処理を書けるなど

といったメリットがあり、総じて可読性、頑強性の意味で使いやすい。

余談

メソッドチェーンや、dplyrのパイプ演算子(%>%)は関数型プログラミング的な概念なのかなー。
jupyterで初心者が書きがちなのは手続き型。

www.headboost.jp

qiita.com

kiito.hatenablog.com