How Do I Pipe an Expression in Polars?

How do you "pipe" an expression in Polars?

Consider this code:

def transformation(col:pl.Series)->pl.Series:
    return col.tanh().suffix('_tanh')

It'd be nice to be able to do this:

df.with_columns([
   pl.col('colA').pipe(transformation), 
   pl.col('colB').pipe(transformation), 
   pl.col('colC').pipe(transformation), 
   pl.col('colD').pipe(transformation), 
])

But I don't think Polars supports .pipe for Series / expressions.

The alternative is

df.with_columns([
   transformation(pl.col('colA')), 
   transformation(pl.col('colB')), 
   transformation(pl.col('colC')), 
   transformation(pl.col('colD')), 
])

But this gets messy (IMO) when you have arguments to the transformation function

Edit:

I implemented this and it "works" for me

def _pipe(self, func, *args, **kwargs):
    return func(self, *args, **kwargs)

pl.Expr.pipe = _pipe

2 Answers

Typically (like pandas) you'd apply pipe at the DataFrame level.

Especially in conjunction with lazy-eval, this would be equivalent to chaining expressions; your function will receive the underlying eager/lazy frame, along with any optional *args and **kwargs, and by making it lazy() you ensure that your chain of operations can still take advantage of the query optimiser and parallelisation.

For example:

import polars as pl

# define some UDFs
def extend_with_tan( df ):
    return df.with_columns( pl.all().tanh().suffix("_tanh") )

def mul_in_place( df, n ):
    return df.select( (pl.all() * n).suffix(f"_x{n}") )

# init lazyframe 
df = pl.DataFrame({
    "colA": [-4], 
    "colB": [-2], 
    "colC": [10],
}).lazy()

# pipe/result
dfx = df.pipe( extend_with_tan ).pipe( mul_in_place,n=3 )
dfx.collect()

# ┌─────────┬─────────┬─────────┬──────────────┬──────────────┬──────────────┐
# │ colA_x3 ┆ colB_x3 ┆ colC_x3 ┆ colA_tanh_x3 ┆ colB_tanh_x3 ┆ colC_tanh_x3 │
# │ ---     ┆ ---     ┆ ---     ┆ ---          ┆ ---          ┆ ---          │
# │ i64     ┆ i64     ┆ i64     ┆ f64          ┆ f64          ┆ f64          │
# ╞═════════╪═════════╪═════════╪══════════════╪══════════════╪══════════════╡
# │ -12     ┆ -6      ┆ 30      ┆ -2.997988    ┆ -2.892083    ┆ 3.0          │
# └─────────┴─────────┴─────────┴──────────────┴──────────────┴──────────────┘

As you've probably realized, adding custom methods in order to be able to do method chaining is unfortunately not a first-class citizen in python.

In polars, a canonical way that hopefully satisfies you is to instead write a function that returns an expression. You do this already (although the type hint is incorrectly set to pl.Series), but can save some space by giving a string argument to our transformation function:

import polars as pl

df = pl.DataFrame({"colA": [-4], "colB": [-2], "colC": [0], "colD": [2]})


def transformation(name: str | list[str]) -> pl.Expr:
    return pl.col(name).tanh().suffix("_tanh")


df1 = df.with_columns(
    [
        transformation("colA"),
        transformation("colB"),
        transformation("colC"),
        transformation("colD"),
    ]
)

I realise this doesn't quite do what you wanted, but perhaps the following will cheer you up a bit. Since pl.col() can take a list of column names, we can do the following:

df2 = df.with_column(transformation(["colA", "colB", "colC", "colD"]))

assert df1.frame_equal(df2) # True

And we can even target all of them using a regular expression:

# ^col\w+$ is a regular expression matching `col<anything>`
df3 = df.with_column(transformation("^col\w+$"))


assert df1.frame_equal(df3) # True
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Chloe Bennett

Chloe Bennett

Culture, Media & Entertainment Columnist

Chloe Bennett explores the intersection of pop culture, streaming entertainment, digital trends, and contemporary lifestyle. Her weekly commentary reaches thousands of culture enthusiasts.

Share this article
Twitter Facebook Pinterest