dfply python functional-programming dplyr pipeline

dfply - Tubos funcionales en python como%>% de R dplyr



functional-programming pipeline (10)

En R (gracias a dplyr ) ahora puede realizar operaciones con una sintaxis de canalización más funcional a través de %>% . Esto significa que en lugar de codificar esto:

> as.Date("2014-01-01") > as.character((sqrt(12)^2)

También podrías hacer esto:

> "2014-01-01" %>% as.Date > 12 %>% sqrt %>% .^2 %>% as.character

Para mí, esto es más legible y se extiende a casos de uso más allá del marco de datos. ¿El lenguaje python tiene soporte para algo similar?


¿El lenguaje python tiene soporte para algo similar?

La "sintaxis de canalización más funcional" ¿ es realmente una sintaxis más "funcional"? Yo diría que agrega una sintaxis de "infijo" a R en su lugar.

Dicho esto, la gramática de Python no tiene soporte directo para la notación infijo más allá de los operadores estándar.

Si realmente necesitas algo así, deberías tomar ese código de Tomer Filiba como punto de partida para implementar tu propia notación de infijo:

Ejemplo de código y comentarios de Tomer Filiba ( http://tomerfiliba.com/blog/Infix-Operators/ ):

from functools import partial class Infix(object): def __init__(self, func): self.func = func def __or__(self, other): return self.func(other) def __ror__(self, other): return Infix(partial(self.func, other)) def __call__(self, v1, v2): return self.func(v1, v2)

Usando instancias de esta clase peculiar, ahora podemos usar una nueva "sintaxis" para llamar a las funciones como operadores de infijo:

>>> @Infix ... def add(x, y): ... return x + y ... >>> 5 |add| 6


Añadiendo mi 2c. Personalmente uso el paquete fn para la programación de estilo funcional. Tu ejemplo se traduce en

from fn import F, _ from math import sqrt (F(sqrt) >> _**2 >> str)(12)

F es una clase de envoltura con azúcar sintáctica de estilo funcional para aplicación y composición parcial. _ es un constructor de estilo Scala para funciones anónimas (similar a la lambda de Python); representa una variable, por lo tanto, puede combinar varios objetos _ en una expresión para obtener una función con más argumentos (por ejemplo, _ + _ es equivalente a lambda a, b: a + b ). F(sqrt) >> _**2 >> str da como resultado un objeto Callable que puede usarse tantas veces como desee.


Extrañé al operador de tuberías |> de Elixir, así que creé una función simple decorator (~ 50 líneas de código) que reinterpreta el operador de cambio a la derecha de >> Python como una tubería muy similar a Elixir en tiempo de compilación usando la biblioteca ast y la compilación / exec :

from pipeop import pipes def add3(a, b, c): return a + b + c def times(a, b): return a * b @pipes def calc() print 1 >> add3(2, 3) >> times(4) # prints 24

Todo lo que está haciendo es reescribir a >> b(...) como b(a, ...) .

pypi.org/project/pipeop

https://github.com/robinhilliard/pipes


Las tuberías son una nueva característica en Pandas 0.16.2 .

Ejemplo:

import pandas as pd from sklearn.datasets import load_iris x = load_iris() x = pd.DataFrame(x.data, columns=x.feature_names) def remove_units(df): df.columns = pd.Index(map(lambda x: x.replace(" (cm)", ""), df.columns)) return df def length_times_width(df): df[''sepal length*width''] = df[''sepal length''] * df[''sepal width''] df[''petal length*width''] = df[''petal length''] * df[''petal width''] x.pipe(remove_units).pipe(length_times_width) x

NB: La versión Pandas conserva la semántica de referencia de Python. Es por eso que length_times_width no necesita un valor de retorno; modifica x en su lugar.


Puede utilizar la biblioteca sspipe . Expone dos objetos p y px . Similar a x %>% f(y,z) , puede escribir x | p(f, y, z) x | p(f, y, z) y similar a x %>% .^2 puede escribir x | px**2 x | px**2 .

from sspipe import p, px from math import sqrt 12 | p(sqrt) | px ** 2 | p(str)


Si solo quieres esto para los scripts personales, puedes considerar usar Coconut lugar de Python.

El coco es un superconjunto de Python. Por lo tanto, puedes usar el operador de tuberías de Coconut, ignorando completamente el resto del lenguaje Coconut.

Por ejemplo:

def addone(x): x + 1 3 |> addone

compila a

# lots of auto-generated header junk # Compiled Coconut: ----------------------------------------------------------- def addone(x): return x + 1 (addone)(3)


Una posible forma de hacerlo es mediante el uso de un módulo llamado macropy . Macropy te permite aplicar transformaciones al código que has escrito. Por lo tanto a | b a | b puede transformarse en b(a) . Esto tiene una serie de ventajas y desventajas.

En comparación con la solución mencionada por Sylvain Leroux, la principal ventaja es que no necesita crear objetos de infijo para las funciones que está interesado en usar, solo marque las áreas de código que pretende usar en la transformación. En segundo lugar, dado que la transformación se aplica en tiempo de compilación, en lugar de en tiempo de ejecución, el código transformado no sufre sobrecarga durante el tiempo de ejecución: todo el trabajo se realiza cuando el código de byte se produce por primera vez a partir del código fuente.

Las principales desventajas son que la macropía requiere una cierta forma de activación para que funcione (se menciona más adelante). En contraste con un tiempo de ejecución más rápido, el análisis del código fuente es más complejo computacionalmente y, por lo tanto, el programa demorará más en comenzar. Finalmente, agrega un estilo sintáctico que significa que los programadores que no están familiarizados con macropy pueden encontrar su código más difícil de entender.

Código de ejemplo:

run.py

import macropy.activate # Activates macropy, modules using macropy cannot be imported before this statement # in the program. import target # import the module using macropy

target.py

from fpipe import macros, fpipe from macropy.quick_lambda import macros, f # The `from module import macros, ...` must be used for macropy to know which # macros it should apply to your code. # Here two macros have been imported `fpipe`, which does what you want # and `f` which provides a quicker way to write lambdas. from math import sqrt # Using the fpipe macro in a single expression. # The code between the square braces is interpreted as - str(sqrt(12)) print fpipe[12 | sqrt | str] # prints 3.46410161514 # using a decorator # All code within the function is examined for `x | y` constructs. x = 1 # global variable @fpipe def sum_range_then_square(): "expected value (1 + 2 + 3)**2 -> 36" y = 4 # local variable return range(x, y) | sum | f[_**2] # `f[_**2]` is macropy syntax for -- `lambda x: x**2`, which would also work here print sum_range_then_square() # prints 36 # using a with block. # same as a decorator, but for limited blocks. with fpipe: print range(4) | sum # prints 6 print ''a b c'' | f[_.split()] # prints [''a'', ''b'', ''c'']

Y finalmente el módulo que hace el trabajo duro. Lo he llamado fpipe para canalización funcional como su sintaxis de shell emulada para pasar la salida de un proceso a otro.

fpipe.py

from macropy.core.macros import * from macropy.core.quotes import macros, q, ast macros = Macros() @macros.decorator @macros.block @macros.expr def fpipe(tree, **kw): @Walker def pipe_search(tree, stop, **kw): """Search code for bitwise or operators and transform `a | b` to `b(a)`.""" if isinstance(tree, BinOp) and isinstance(tree.op, BitOr): operand = tree.left function = tree.right newtree = q[ast[function](ast[operand])] return newtree return pipe_search.recurse(tree)


Una solución alternativa sería utilizar la herramienta de flujo de trabajo dask. Aunque no es tan sintácticamente divertido como ...

var | do this | then do that

... todavía permite que su variable fluya hacia abajo de la cadena y el uso de dask brinda el beneficio adicional de paralelización cuando sea posible.

Así es como uso dask para lograr un patrón de cadena de tuberías:

import dask def a(foo): return foo + 1 def b(foo): return foo / 2 def c(foo,bar): return foo + bar # pattern = ''name_of_behavior'': (method_to_call, variables_to_pass_in, variables_can_be_task_names) workflow = {''a_task'':(a,1), ''b_task'':(b,''a_task'',), ''c_task'':(c,99,''b_task''),} #dask.visualize(workflow) #visualization available. dask.get(workflow,''c_task'') # returns 100

Después de haber trabajado con elixir, quería usar el patrón de tuberías en Python. Este no es exactamente el mismo patrón, pero es similar y, como dije, viene con beneficios adicionales de paralelización; Si le dice a dask que obtenga una tarea en su flujo de trabajo que no depende de que otros ejecuten primero, se ejecutarán en paralelo.

Si quisiera una sintaxis más sencilla, podría envolverla en algo que se ocupe de la asignación de nombres de las tareas por usted. Por supuesto, en esta situación, necesitaría todas las funciones para tomar la tubería como primer argumento y perdería cualquier beneficio de la paralización. Pero si estás de acuerdo con eso, podrías hacer algo como esto:

def dask_pipe(initial_var, functions_args): '''''' call the dask_pipe with an init_var, and a list of functions workflow, last_task = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]}) workflow, last_task = dask_pipe(initial_var, [function_1, function_2]) dask.get(workflow, last_task) '''''' workflow = {} if isinstance(functions_args, list): for ix, function in enumerate(functions_args): if ix == 0: workflow[''task_'' + str(ix)] = (function, initial_var) else: workflow[''task_'' + str(ix)] = (function, ''task_'' + str(ix - 1)) return workflow, ''task_'' + str(ix) elif isinstance(functions_args, dict): for ix, (function, args) in enumerate(functions_args.items()): if ix == 0: workflow[''task_'' + str(ix)] = (function, initial_var) else: workflow[''task_'' + str(ix)] = (function, ''task_'' + str(ix - 1), *args ) return workflow, ''task_'' + str(ix) # piped functions def foo(df): return df[[''a'',''b'']] def bar(df, s1, s2): return df.columns.tolist() + [s1, s2] def baz(df): return df.columns.tolist() # setup import dask import pandas as pd df = pd.DataFrame({''a'':[1,2,3],''b'':[1,2,3],''c'':[1,2,3]})

Ahora, con esta envoltura, puede hacer una tubería siguiendo cualquiera de estos patrones sintácticos:

# wf, lt = dask_pipe(initial_var, [function_1, function_2]) # wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})

Me gusta esto:

# test 1 - lists for functions only: workflow, last_task = dask_pipe(df, [foo, baz]) print(dask.get(workflow, last_task)) # returns [''a'',''b''] # test 2 - dictionary for args: workflow, last_task = dask_pipe(df, {foo:[], bar:[''string1'', ''string2'']}) print(dask.get(workflow, last_task)) # returns [''a'',''b'',''string1'',''string2'']



pipe construcción con Infix

Como lo insinuó Sylvain Leroux , podemos usar el operador de Infix para construir una pipe infijo. Veamos cómo se logra esto.

Primero, aquí está el código de Tomer Filiba.

Ejemplo de código y comentarios de Tomer Filiba ( http://tomerfiliba.com/blog/Infix-Operators/ ):

from functools import partial class Infix(object): def __init__(self, func): self.func = func def __or__(self, other): return self.func(other) def __ror__(self, other): return Infix(partial(self.func, other)) def __call__(self, v1, v2): return self.func(v1, v2)

Usando instancias de esta clase peculiar, ahora podemos usar una nueva "sintaxis" para llamar a las funciones como operadores de infijo:

>>> @Infix ... def add(x, y): ... return x + y ... >>> 5 |add| 6

El operador de tubería pasa el objeto anterior como un argumento al objeto que sigue a la tubería, por lo que x %>% f se puede transformar en f(x) . En consecuencia, el operador de pipe se puede definir usando Infix siguiente manera:

In [1]: @Infix ...: def pipe(x, f): ...: return f(x) ...: ...: In [2]: from math import sqrt In [3]: 12 |pipe| sqrt |pipe| str Out[3]: ''3.4641016151377544''

Una nota sobre aplicación parcial.

El operador %>% de dpylr argumentos en el primer argumento de una función, por lo que

df %>% filter(x >= 2) %>% mutate(y = 2*x)

corresponde a

df1 <- filter(df, x >= 2) df2 <- mutate(df1, y = 2*x)

La forma más fácil de lograr algo similar en Python es usar el currying . La biblioteca de toolz proporciona una función de decoración de curry que facilita la construcción de funciones de curry.

In [2]: from toolz import curry In [3]: from datetime import datetime In [4]: @curry def asDate(format, date_string): return datetime.strptime(date_string, format) ...: ...: In [5]: "2014-01-01" |pipe| asDate("%Y-%m-%d") Out[5]: datetime.datetime(2014, 1, 1, 0, 0)

Tenga en cuenta que |pipe| inserta los argumentos en la posición del último argumento , es decir

x |pipe| f(2)

corresponde a

f(2, x)

Al diseñar funciones con curry, los argumentos estáticos (es decir, los argumentos que podrían usarse para muchos ejemplos) deben ubicarse antes en la lista de parámetros.

Tenga en cuenta que toolz incluye muchas funciones previamente toolz , incluidas varias funciones del módulo del operator .

In [11]: from toolz.curried import map In [12]: from toolz.curried.operator import add In [13]: range(5) |pipe| map(add(2)) |pipe| list Out[13]: [2, 3, 4, 5, 6]

que corresponde aproximadamente a lo siguiente en R

> library(dplyr) > add2 <- function(x) {x + 2} > 0:4 %>% sapply(add2) [1] 2 3 4 5 6

Usando otros delimitadores de infijo

Puede cambiar los símbolos que rodean la invocación de Infix anulando otros métodos de operador de Python. Por ejemplo, cambiar __or__ y __ror__ a __mod__ y __rmod__ cambiará el | Operador al operador mod .

In [5]: 12 %pipe% sqrt %pipe% str Out[5]: ''3.4641016151377544''