大まかに、Python/Pandasを使用してRから dplyr パッケージを(学習課題として)複製しようとしています。私が行き詰まっているのは、「パイピング」機能です。
R/dplyrでは、これはパイプ演算子_%>%
_を使用して行われます。ここで、x %>% f(y)
はf(x, y)
と同等です。可能であれば、中置構文を使用してこれを複製したいと思います( ここ を参照)。
説明のために、以下の2つの関数を検討してください。
_import pandas as pd
def select(df, *args):
cols = [x for x in args]
df = df[cols]
return df
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
_
最初の関数はデータフレームを受け取り、指定された列のみを返します。 2番目はデータフレームを受け取り、指定された列の名前を変更します。例えば:
_d = {'one' : [1., 2., 3., 4., 4.],
'two' : [4., 3., 2., 1., 3.]}
df = pd.DataFrame(d)
# Keep only the 'one' column.
df = select(df, 'one')
# Rename the 'one' column to 'new_one'.
df = rename(df, one = 'new_one')
_
Pipe/infix構文を使用して同じことを実現するには、コードは次のようになります。
_df = df | select('one') \
| rename(one = 'new_one')
_
したがって、_|
_の左側からの出力は、右側の関数の最初の引数として渡されます。私がこのようなことが行われるのを見るときはいつでも( here など)、ラムダ関数が含まれます。同じように関数間でパンダのデータフレームをパイプすることは可能ですか?
Pandasには_.pipe
_メソッドが含まれていますが、私にとって重要なのは、提供した例の構文です。参考になれば幸いです。
_pandas.DataFrame
_が実装するため、ビット単位のor
演算子を使用してこれを実装することは困難です。 _|
_を_>>
_に置き換えてもかまわない場合は、次のように試すことができます。
_import pandas as pd
def select(df, *args):
cols = [x for x in args]
return df[cols]
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
class SinkInto(object):
def __init__(self, function, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.function = function
def __rrshift__(self, other):
return self.function(other, *self.args, **self.kwargs)
def __repr__(self):
return "<SinkInto {} args={} kwargs={}>".format(
self.function,
self.args,
self.kwargs
)
df = pd.DataFrame({'one' : [1., 2., 3., 4., 4.],
'two' : [4., 3., 2., 1., 3.]})
_
次に、次のことができます。
_>>> df
one two
0 1 4
1 2 3
2 3 2
3 4 1
4 4 3
>>> df = df >> SinkInto(select, 'one') \
>> SinkInto(rename, one='new_one')
>>> df
new_one
0 1
1 2
2 3
3 4
4 4
_
Python 3では、ユニコードを乱用することができます:
_>>> print('\u01c1')
ǁ
>>> ǁ = SinkInto
>>> df >> ǁ(select, 'one') >> ǁ(rename, one='new_one')
new_one
0 1
1 2
2 3
3 4
4 4
_
[更新]
お返事をありがとうございます。関数を引数として渡さなくても済むように、関数ごとに個別のクラス(SinkIntoなど)を作成することはできますか?
_def pipe(original):
class PipeInto(object):
data = {'function': original}
def __init__(self, *args, **kwargs):
self.data['args'] = args
self.data['kwargs'] = kwargs
def __rrshift__(self, other):
return self.data['function'](
other,
*self.data['args'],
**self.data['kwargs']
)
return PipeInto
@pipe
def select(df, *args):
cols = [x for x in args]
return df[cols]
@pipe
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
_
これで、DataFrame
を最初の引数として取る関数を装飾できます。
_>>> df >> select('one') >> rename(one='first')
first
0 1
1 2
2 3
3 4
4 4
_
Rubyのような言語は「表現力豊か」なので、すべてのプログラムを新しいDSLとして書くことを奨励しますが、これは一種のPythonでは不快です。多くのPythonistは異なる罪深い冒涜としての目的。
ユーザーOHLÁLÁは感銘を受けていません:
このソリューションの問題は、パイピングではなく関数を呼び出そうとするときです。 –OHLÁLÁ
Dunder-callメソッドを実装できます。
_def __call__(self, df):
return df >> self
_
その後:
_>>> select('one')(df)
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
_
OHLÁLÁを喜ばせるのは簡単ではないようです:
その場合は、オブジェクトを明示的に呼び出す必要があります。
select('one')(df)
これを回避する方法はありますか? –OHLÁLÁ
まあ、私は解決策を考えることができますが、警告があります:元の関数は、pandas dataframe(キーワード引数は大丈夫です)である2番目の位置引数を取ってはなりません。___new__
_メソッドをドコレーター内のPipeInto
クラスに追加し、最初の引数がデータフレームであるかどうかをテストします。そうである場合は、引数を指定して元の関数を呼び出します。
_def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], pd.DataFrame):
return cls.data['function'](*args, **kwargs)
return super().__new__(cls)
_
それはうまくいくようですが、おそらく私が見つけることができなかったいくつかの欠点があります。
_>>> select(df, 'one')
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
>>> df >> select('one')
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
_
Pythonで dplyr を使用することは、Python(これにはrshift演算子がありますが、ギミックとして、オブジェクト属性としてメソッドではなくジェネリック関数を使用しているため、パイプ演算子はRでのみ必要になる可能性があることも指摘しておきます Method chaining は、演算子をオーバーライドする必要なく、基本的に同じものを提供します。
dataf = (DataFrame(mtcars).
filter('gear>=3').
mutate(powertoweight='hp*36/wt').
group_by('gear').
summarize(mean_ptw='mean(powertoweight)'))
括弧のペアの間にチェーンをラップすることで、各行の末尾に\
を付けなくても、チェーンを複数の行に分割できることに注意してください。
sspipe ライブラリを使用でき、次の構文を使用できます。
from sspipe import p
df = df | p(select, 'one') \
| p(rename, one = 'new_one')
これを行う組み込みの方法が見つからなかったため、__call__
をサポートしているため、*args/**kwargs
演算子を使用するクラスを作成しました。
class Pipe:
def __init__(self, value):
"""
Creates a new pipe with a given value.
"""
self.value = value
def __call__(self, func, *args, **kwargs):
"""
Creates a new pipe with the value returned from `func` called with
`args` and `kwargs` and it's easy to save your intermedi.
"""
value = func(self.value, *args, **kwargs)
return Pipe(value)
構文は多少慣れる必要がありますが、パイピングが可能です。
def get(dictionary, key):
assert isinstance(dictionary, dict)
assert isinstance(key, str)
return dictionary.get(key)
def keys(dictionary):
assert isinstance(dictionary, dict)
return dictionary.keys()
def filter_by(iterable, check):
assert hasattr(iterable, '__iter__')
assert callable(check)
return [item for item in iterable if check(item)]
def update(dictionary, **kwargs):
assert isinstance(dictionary, dict)
dictionary.update(kwargs)
return dictionary
x = Pipe({'a': 3, 'b': 4})(update, a=5, c=7, d=8, e=1)
y = (x
(keys)
(filter_by, lambda key: key in ('a', 'c', 'e', 'g'))
(set)
).value
z = x(lambda dictionary: dictionary['a']).value
assert x.value == {'a': 5, 'b': 4, 'c': 7, 'd': 8, 'e': 1}
assert y == {'a', 'c', 'e'}
assert z == 5