StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python でパイプ演算子を使いたい


ネタ記事です。/ This is a joke post which makes no practical sense.


はじめに

Python pandas では主要な操作を以下のようにメソッドチェインの形で書くことができる。

# Python (pandas)
df.assign(x=df['y'] + df['z']).groupby('x').sum()

pandas v0.16.2 で DataFrameSeries.pipe というメソッドが追加され、このチェインを外部の関数/メソッドに対して連結できるようになった。利用例は以下のリンクを。

補足 matplotlib でも v1.5.0 で ラベルデータ対応 が追加され、各関数が .pipe から利用できるようになる予定。

このメソッドチェインによる "処理の連結" を、R ( {magrittr} ) のようにパイプ演算子 %>% を使って統一的に書きたい。

# R (magrittr + dplyr)
df %>% mutate(x = y + z) %>% group_by(x) %>% summarize_each(funs(sum))

R と異なり、 Python では自作の演算子を定義することはできない。Python演算子の中では 右方向ビットシフト演算子 >> がパイプ演算子っぽく見えなくもない。これを使って R のような動作をさせたい。

演算子のオーバーライド

まずはサンプルデータを作成する。

import numpy as np
import pandas as pd
pd.__version__
# '0.16.2'

df = pd.DataFrame({'X': [1, 2, 3, 4, 5],
                   'Y': ['a', 'b', 'a', 'b', 'a']})
df
#    X  Y
# 0  1  a
# 1  2  b
# 2  3  a
# 3  4  b
# 4  5  a

普通に .pipe を使うとこのような感じになる。

func = lambda x: x
df.pipe(func)
#    X  Y
# 0  1  a
# 1  2  b
# 2  3  a
# 3  4  b
# 4  5  a

この処理を >> 演算子を使って書きたい。そのままでは、演算方法が未定義のため TypeError となる。

df >> func
# TypeError: unsupported operand type(s) for >>: 'DataFrame' and 'function'

Python では各演算子に対応する処理はマジックメソッドとして実装する。そのため、>> に対応する __rshift__.pipe と同じ処理をさせればよい。また、R のように 右辺には関数を渡したいので、pandas のメソッドを適当にラップする関数 pipify を定義する。

pd.DataFrame.__rshift__ = pd.DataFrame.pipe

def pipify(f):
    def wrapped(*args, **kwargs):
        return lambda self: f(self, *args, **kwargs)
    return wrapped

head = pipify(pd.DataFrame.head)

できた。

df >> head(n=2)
#    X  Y
# 0  1  a
# 1  2  b

groupby も同じように書ける。集約関数は DataFrameDataFrameGroupBy 双方から使うため以下のように定義する。

groupby = pipify(pd.DataFrame.groupby)

def sum(*args, **kwargs):
    return lambda self: self.sum(*args, **kwargs)

また、groupby した結果からも >> で演算を連結したい。そのためにDataFrameGroupBy.__rshift__ を定義する。

def _pipe(self, func):
    return func(self)

pd.core.groupby.DataFrameGroupBy.__rshift__ = _pipe

できた。

df >> groupby('Y') >> sum()
#    X
# Y   
# a  9
# b  6

df >> sum()
# X       15
# Y    ababa
# dtype: object

右辺側クラスによるオーバーライド

DataFrame が左辺にある場合、パイプ演算は上記のように定義できた。加えて、DataFrame が右辺にある場合も同じようなことがしたい。

以下のような記法で、左辺の辞書から DataFrame を作成したい。これも (当然) そのままではできない。

data = {'X': [1, 2, 3, 4, 5],
        'Y': ['a', 'b', 'a', 'b', 'a']} 

data >> pd.DataFrame()
# TypeError: unsupported operand type(s) for >>: 'dict' and 'DataFrame'

Python では、左辺側に 右辺を処理できるマジックメソッドがない場合は、右辺側のマジックメソッド ( DataFarme.__rrshift__ ) が利用される。これを適当に定義すればよい。

def _get(a, b):
    return pd.DataFrame(b)

pd.DataFrame.__rrshift__ = _get

できた。

data = {'X': [1, 2, 3, 4, 5],
        'Y': ['a', 'b', 'a', 'b', 'a']} 

data >> pd.DataFrame()
#    X  Y
# 0  1  a
# 1  2  b
# 2  3  a
# 3  4  b
# 4  5  a

NumPy に対するオーバーライド

DataFramenp.ndarray からも作成することができる。が、>> ではうまく動かない。

data = np.array([[1, 2], [3, 4]])

pd.DataFrame(data)
#    0  1
# 0  1  2
# 1  3  4

data >> pd.DataFrame()
# TypeError: ufunc 'right_shift' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

ndarray のマジックメソッドは スカラーndarray ライクな引数を汎用的に処理できるように定義されている。具体的には、引数が ndarray のサブクラス、もしくは Array Interface ( __array__ ) をもつとき、ndarray によって処理が行われる。

DataFramendarray のサブクラスではないが Array Interface をもつ。右辺が不正なため通常の例外が送出されてしまい、右辺側のマジックメソッドに処理が移らない。

参考 Subclassing ndarray — NumPy v1.11 Manual

issubclass(pd.DataFrame, np.ndarray)
# False

df.__array__()
# array([[1, 'a'],
#        [2, 'b'],
#        [3, 'a'],
#        [4, 'b'],
#        [5, 'a']], dtype=object)

ここで DataFrame 側のマジックメソッドを利用したい場合、上記リンクに記載されている __array_priority__ プロパティを設定すればよい。DataFrame 側の優先度を上げると期待した処理が行われることがわかる。

pd.DataFrame.__array_priority__ = 1000

data >> pd.DataFrame()
#    0  1
# 0  1  2
# 1  3  4

まとめ

R 使ったほうがいい。Use R!

補足 こんなパッケージもあります。

2016/10/18 追記

続き(?)です。

sinhrks.hatenablog.com