Salida "Piping" de una función a otra usando la sintaxis de infix de Python
pipeline infix-notation (4)
Es difícil implementar esto usando bitwise or
operator porque pandas.DataFrame
implementa. Si no te importa reemplazar |
con >>
, puedes probar esto:
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.]})
Entonces puedes hacer:
>>> 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
En Python 3 puedes abusar de unicode:
>>> print(''/u01c1'')
ǁ
>>> ǁ = SinkInto
>>> df >> ǁ(select, ''one'') >> ǁ(rename, one=''new_one'')
new_one
0 1
1 2
2 3
3 4
4 4
[actualizar]
Gracias por su respuesta. ¿Sería posible crear una clase separada (como SinkInto) para cada función para evitar tener que pasar las funciones como un argumento?
¿Qué tal un decorador?
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
Ahora puedes decorar cualquier función que tome un DataFrame
como primer argumento:
>>> df >> select(''one'') >> rename(one=''first'')
first
0 1
1 2
2 3
3 4
4 4
Python es impresionante!
Sé que los lenguajes como Ruby son "tan expresivos" que alientan a las personas a escribir cada programa como un nuevo DSL, pero esto está algo mal visto en Python. Muchos pitonistas consideran la sobrecarga de operadores para un propósito diferente como una blasfemia pecaminosa.
[actualizar]
El usuario OHLÁLÁ no está impresionado:
El problema con esta solución es cuando intenta llamar a la función en lugar de canalizar. - OH LA LA
Puedes implementar el método dunder-call:
def __call__(self, df):
return df >> self
Y entonces:
>>> select(''one'')(df)
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
Parece que no es fácil complacer a OHLÁLÁ:
En ese caso necesita llamar explícitamente al objeto:
select(''one'')(df)
¿Hay alguna manera de evitar eso? - OH LA LA
Bueno, puedo pensar en una solución pero hay una advertencia: su función original no debe tomar un segundo argumento posicional que es un marco de datos de pandas (los argumentos de palabras clave están bien). Permite agregar un método __new__
a nuestra clase PipeInto
dentro del docorator que comprueba si el primer argumento es un marco de datos, y si lo es, simplemente llamamos a la función original con los argumentos:
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], pd.DataFrame):
return cls.data[''function''](*args, **kwargs)
return super().__new__(cls)
Parece funcionar, pero probablemente hay un inconveniente que no pude ver.
>>> 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
Estoy tratando de replicar, aproximadamente, el paquete dplyr de R usando Python / Pandas (como un ejercicio de aprendizaje). Algo en lo que estoy atascado es la funcionalidad de "tuberías".
En R / dplyr, esto se hace usando el operador de tubería %>%
, donde x %>% f(y)
es equivalente a f(x, y)
. Si es posible, me gustaría replicar esto utilizando la sintaxis de infijo (ver here ).
Para ilustrar, considere las dos funciones a continuación.
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
La primera función toma un marco de datos y devuelve solo las columnas dadas. El segundo toma un marco de datos y renombra las columnas dadas. Por ejemplo:
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'')
Para lograr la misma sintaxis de tubería / infijo, el código sería:
df = df | select(''one'') /
| rename(one = ''new_one'')
Así que la salida desde el lado izquierdo de |
se pasa como el primer argumento de la función en el lado derecho. Cada vez que veo algo como esto hecho ( here , por ejemplo) involucra funciones lambda. ¿Es posible canalizar un marco de datos de Pandas entre funciones de la misma manera?
Sé que Pandas tiene el método .pipe
, pero lo que es importante para mí es la sintaxis del ejemplo que proporcioné. Cualquier ayuda sería apreciada.
No pude encontrar una forma integrada de hacer esto, así que creé una clase que usa el operador __call__
porque admite *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)
La sintaxis requiere algo de tiempo para acostumbrarse, pero permite la canalización.
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
Puede usar la biblioteca sspipe y la siguiente sintaxis:
from sspipe import p
df = df | p(select, ''one'') /
| p(rename, one = ''new_one'')
Si bien no puedo dejar de mencionar que usar dplyr en Python podría ser lo más parecido a tener dplyr en Python (tiene el operador de cambio de turno, pero como un truco), también me gustaría señalar que el operador de tuberías solo puede ser necesario en R debido a su uso de funciones genéricas en lugar de métodos como atributos de objeto. El encadenamiento de métodos le da esencialmente lo mismo sin tener que anular los operadores:
dataf = (DataFrame(mtcars).
filter(''gear>=3'').
mutate(powertoweight=''hp*36/wt'').
group_by(''gear'').
summarize(mean_ptw=''mean(powertoweight)''))
La nota que envuelve la cadena entre un par de paréntesis le permite dividirla en varias líneas sin la necesidad de un final en cada línea.