我正在尝试使用Python / Pandas(作为学习练习)粗略地复制R中的dplyr包。我坚持的是“管道”功能。
在R / dplyr中,这是使用管道操作符%>%
完成的,其中x %>% f(y)
等同于f(x, y)
。如果可能,我想使用中缀语法复制它(请参阅here)。
为了说明,请考虑以下两个函数。
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
第一个函数采用数据帧并仅返回给定的列。第二个采用数据帧,并重命名给定的列。例如:
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')
要使用管道/中缀语法实现相同的目的,代码将是:
df = df | select('one') \
| rename(one = 'new_one')
因此|
左侧的输出作为第一个参数传递给右侧的函数。每当我看到这样的事情(例如here)时,它就会涉及到lambda函数。是否可以以相同的方式在函数之间管道Pandas的数据帧?
我知道Pandas有.pipe
方法,但对我来说重要的是我提供的示例的语法。任何帮助,将不胜感激。
答案 0 :(得分:18)
使用按位or
运算符很难实现这一点,因为pandas.DataFrame
实现了它。如果您不介意将|
替换为>>
,则可以尝试以下操作:
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中,您可以滥用unicode:
>>> 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中是不受欢迎的。许多Python教徒认为运算符重载是出于不同目的而作为一种罪恶的亵渎。
用户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数据帧(关键字参数没问题)。让我们在docorator中的__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
答案 1 :(得分:9)
虽然我无法提及使用dplyr in Python可能最接近Python中的dplyr(它有rshift运算符,但作为噱头),我也想指出管道运算符可能只在R中是必需的,因为它使用泛型函数而不是方法作为对象属性。 Method chaining基本相同,无需覆盖运算符:
dataf = (DataFrame(mtcars).
filter('gear>=3').
mutate(powertoweight='hp*36/wt').
group_by('gear').
summarize(mean_ptw='mean(powertoweight)'))
注意在一对括号之间包裹链可以将它分成多行,而不需要在每一行上都有尾随\
。
答案 2 :(得分:1)
您可以使用sspipe库,并使用以下语法:
from sspipe import p
df = df | p(select, 'one') \
| p(rename, one = 'new_one')
答案 3 :(得分:1)
我一直在从 Python 中的 R 移植数据包(dplyr、tidyr、tibble 等):
https://github.com/pwwang/datar
如果您熟悉 R 中的那些包,并想将其应用到 Python 中,那么它就在这里为您提供:
void findAns(int n, int peg1, int peg2, int peg3)
{
if(n<1)
return;
findAns(n-1,peg1,peg2,peg3);
printf("Move(%d->%d)n",peg1,peg3);
findAns(n-1,peg2,peg1,peg3);
printf("Move(%d->%d)n",peg3,peg2);
findAns(n-1,peg1,peg2,peg3);
}
输出:
from datar.all import *
d = {'one' : [1., 2., 3., 4., 4.],
'two' : [4., 3., 2., 1., 3.]}
df = tibble(one=d['one'], two=d['two'])
df = df >> select(f.one) >> rename(new_one=f.one)
print(df)
答案 4 :(得分:0)
我无法找到这样做的内置方式,所以我创建了一个使用__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
答案 5 :(得分:0)
我会坚决反对这样做或此处提出的任何答案,而只是在标准python代码中实现pipe
函数,而不会引起操作员的欺骗,装饰或其他错误:
def pipe(first, *args):
for fn in args:
first = fn(first)
return first
在此处查看我的答案以获取更多背景信息: https://stackoverflow.com/a/60621554/2768350
重载运算符,涉及外部库,而那些不能使代码的可读性差,可维护性差,可测试性差和Python语言少的东西。
如果我想在python中进行某种形式的管道操作,我只想做pipe(input, fn1, fn2, fn3)
。那是我能想到的最可读和最强大的解决方案。
如果我们公司中的某人将操作员重载或对生产 just 的新依赖项进行了处理,则该管道将立即恢复,并且他们将被判处在本周剩余时间内进行质量检查:D
如果您真的真的必须对管道使用某种运算符,那么也许您遇到了更大的问题,并且Python不是您的用例的正确语言...
答案 6 :(得分:0)
一个老问题,但我仍然很感兴趣(来自 R)。因此,尽管受到纯粹主义者的反对,这里还是一个受 http://tomerfiliba.com/blog/Infix-Operators/
启发的矮个子class FuncPipe:
class Arg:
def __init__(self, arg):
self.arg = arg
def __or__(self, func):
return func(self.arg)
def __ror__(self, arg):
return self.Arg(arg)
pipe = FuncPipe()
然后
1 |pipe| \
(lambda x: return x+1) |pipe| \
(lambda x: return 2*x)
返回
4