Apply a custom function with full LazyFrame context.
fnCallable[[pl.LazyFrame, pl.Expr], pl.Expr]
Called as fn(lf, col_ref) → pl.Expr for each matched column. col_ref evaluates to the column’s values in lf; it works correctly for any expression shape, including transforms and when/then/otherwise. Use col_ref.meta.output_name() when the string column name is needed.
Examples:
def center_scale(lf: pl.LazyFrame, col_ref: pl.Expr) -> pl.Expr: stats = lf.select( col_ref.mean().alias("m"), col_ref.std().alias("s") ).collect() m, s = stats["m"][0], stats["s"][0]return (col_ref - m) / spl.DataFrame({"x": [1.0, 2.0, 3.0, 4.0, 5.0]}).ps.with_columns( pl.col("x").ps.apply(center_scale))
Cast a column to Enum, optionally deriving categories from the data.
When categories are derived from the data, they are sorted by the column’s native dtype before being cast to string. This means integers sort numerically (1, 2, 10), dates chronologically, and strings alphabetically — rather than all sorting lexicographically.
categoriesSequence[str] | None
Fixed set of allowed values. If omitted, derived from the data as the unique values sorted by native dtype order.
Optional callable that receives the non-null counts DataFrame (columns: category column + "n", sorted by frequency descending) and returns a boolean sequence where True marks categories to collapse.
With .over(): each group’s order is computed from its own data, then categories are unioned into one shared Enum’s declared category list — so a value’s level (chained via .ps_enum.to_level()) is always locally correct, but the Enum dtype’s own declared category order is one overall order, not any single group’s.
Expr.ps_enum.set_categories
Expr.ps_enum.set_categories( categories,)
Set the exact category list. Values not in categories become null.
Insert new categories without changing any values.
categoriesSequence[str]
New category labels to add.
beforeint | None
Insert before this 0-based index of the existing categories. None (default) appends at the end. Negative indices count from the end. Any value ≥ len(categories) is equivalent to None (end).
Move specified categories to a given position, keeping all others in their relative order.
*levelsstr = ()
beforeint | None = 0
Insert before this 0-based index of the remaining categories. 0 (default) moves to the front. None appends at the end. Negative indices count from the end of the remaining categories. Any value ≥ len(remaining) is equivalent to None (end).
Returns an Enum-typed column whose category names are the bin labels. Integer columns use fully-closed [a, b] notation; single-element bins are written as {x}.
Category labels (must be len(breaks) + 1). Auto-generated if omitted.
left_closedbool = True
If True (default), intervals are [lo, hi); otherwise (lo, hi].
fmtstr | Callable | None
Formatter for auto-generated labels. For numeric, a format-spec string (e.g. “.2f”) or callable. For temporal, a callable or None (uses str()).
extendbool = True
For numeric only — if True (default), outermost labels extend to -∞/+∞. For unsigned integers: 0/+∞. If False, uses data min/max. Temporal breaks always use data bounds regardless of this setting.
return_structbool = False
If True, return a struct {lo, hi} instead of just the label.
Expr.ps_chop.n_elements( n, tail='split', labels=None, left_closed=True, fmt='g', extend=False, return_struct=False,)
Chop into groups of n observations each.
Returns an Enum-typed column whose category names are the bin labels. Boundaries are drawn after every nth element (sorted order). Ties are never split — the boundary advances to the next distinct value if needed.
nint
Number of observations per group.
tailLiteral['split', 'merge'] = 'split'
What to do when the total doesn’t divide evenly. “split” (default) keeps the smaller final group; “merge” absorbs it into the preceding group.
labelsSequence[str] | None
Category labels. Auto-generated if omitted.
left_closedbool = True
If True (default), intervals are [lo, hi); otherwise (lo, hi].
fmtstr | Callable[[float], str] = 'g'
Number formatter for auto-generated labels (numeric columns only).
extendbool = False
If True, extend outermost labels to -∞ / +∞ (or 0 / +∞ for unsigned integers). If False (default), the first label opens at the data minimum and the last closes at the data maximum.
return_structbool = False
If True, return a struct instead of just the label.
Returns an Enum-typed column whose category names are the bin labels.
probsSequence[float]
Quantile probabilities in (0, 1), e.g. [0.25, 0.5, 0.75] for quartiles.
labelsSequence[str] | None
Category labels (must be len(probs) + 1). Auto-generated if omitted.
left_closedbool = True
If True (default), intervals are [lo, hi); otherwise (lo, hi].
fmtstr | Callable | None
Formatter for auto-generated labels. For numeric, defaults to “.0%” (percentages) when raw=False and “g” when raw=True. For temporal, a callable or None (uses str()).
rawbool = False
If True, label with the actual break values instead of percentages. Ignored for temporal columns (always uses actual values).
extendbool = False
If True, extend outermost labels to -∞ / +∞ (only affects numeric raw=True). Default False. For unsigned columns, lower bound is 0.
return_structbool = False
If True, return a struct instead of just the label.
Format this column’s values into template, the way str.format formats a value.
For a plain (non-Struct) column, this is a one-argument shorthand for ps.format(template, self) — template contains exactly one {...} field, referring to this expression’s own values. For a Struct column, each field is instead unpacked as a named argument, keyed by its field name — template can then reference each one by name, the same way ps.format(template, **fields) would.
Truncate each string to fit within width characters.
Collapses whitespace and appends placeholder when the text is cut.
widthint = 5
Maximum length of the result, including the placeholder.
sideLiteral['right', 'left', 'center'] = 'right'
Which side to truncate — ‘right’ (default), ‘left’, or ‘center’.
placeholderstr = '…'
String inserted where the text is cut.
Examples:
pl.DataFrame({"x": ["short", "a much longer string"]}).select( pl.col("x").ps_str.trunc(width=10))
shape: (2, 1)
┌────────────┐
│ x │
│ --- │
│ str │
╞════════════╡
│ short │
│ a much lo… │
└────────────┘
Function helpers
ps.F
ps.F( fn,)
Turn an arbitrary function into something callable on expressions, with real data.
fn is called exactly once, eagerly, with the complete data (as pl.Series) for its pl.Expr arguments — never a sample, dummy data, or a batch/slice. That means no return_dtype is needed: the output dtype is whatever fn actually produced. This is the right default for arbitrary vectorized functions (numpy, scipy, …) that don’t otherwise fit Polars’ expression API.
Only pl.Expr arguments (positional or keyword) are resolved against the data; any other argument (a plain string, number, …) is forwarded to fn unchanged — useful for a function’s non-column parameters, e.g. a cluster count.
The cost is an eager .collect() of fn’s pl.Expr arguments at the point ps.F(fn)(...) is resolved (via ps.with_columns/ps.select), the same tradeoff ps_chop and ps_enum already make for operations that need to see real data. Preceding .filter()/.select() calls are still pushed down, but nothing after this point can narrow the collect retroactively.
For a function that must stay fully lazy (e.g. inside a .over() or streaming pipeline) and can tolerate being called on batches/slices instead of the full column, use ps.B instead. For a plain Python function that only accepts scalars (not arrays), use ps.E.
Turn an arbitrary vectorized function into something callable on expressions, lazily.
Thin wrapper around pl.map_batches: fn’s pl.Expr arguments (positional or keyword) are resolved to pl.Series, but — unlike ps.F — it may be called more than once, and on batches/slices rather than the complete column (e.g. under streaming execution, or once per group inside .over()/group_by().agg()). In exchange, the result stays fully lazy: no eager collect is forced at the call site. As with ps.F, any non-pl.Expr argument is forwarded to fn unchanged.
If return_dtype is left unset, Polars infers it by calling fn once with synthetic dummy data — this can raise for domain-restricted functions, or infer the wrong dtype. Prefer ps.F unless you specifically need laziness/streaming and can either supply return_dtype or tolerate that inference step.
fnCallable
return_dtypepl.DataTypeExpr | pl.DataType | None
is_elementwisebool = False
**map_batches_kwargs = {}
Examples:
import numpy as npimport polars as plimport polarstation as psdf = pl.DataFrame({"log_p": [-0.5, -3.0], "log_q": [-1.2, -0.4]})df.lazy().with_columns( combined=ps.B(np.logaddexp, return_dtype=pl.Float64)(pl.col("log_p"), pl.col("log_q"))).collect()
Turn a scalar (non-vectorized) Python function into something callable on expressions.
fn is called once per row with plain Python scalars — the multi-argument equivalent of pl.Expr.map_elements, built on pl.struct(...).map_elements(...). As with ps.F/ps.B, only pl.Expr arguments (positional or keyword) become per-row values; any other argument is forwarded to fn unchanged. Note that skip_nulls (defaults to True, can be overridden via **map_elements_kwargs) only skips a row when the entire struct is null, which struct values built from columns essentially never are — a null in a single argument still reaches fn as None, so fn must be able to handle that itself if any argument column has nulls.
Unlike ps.F/ps.B, there is no vectorized fast path here — fn runs once per row. Only reach for ps.E when fn genuinely cannot operate on whole arrays at once (e.g. it calls into a scalar-only library). For anything that accepts numpy arrays or pl.Series directly, prefer ps.F.
fnCallable
return_dtypepl.DataTypeExpr | pl.DataType | None
**map_elements_kwargs = {}
Examples:
import polars as plimport polarstation as psdef levenshtein(a, b):iflen(a) <len(b): a, b = b, a prev =list(range(len(b) +1))for i, ca inenumerate(a, 1): curr = [i] + [0] *len(b)for j, cb inenumerate(b, 1): curr[j] =min(prev[j] +1, curr[j -1] +1, prev[j -1] + (ca != cb)) prev = currreturn prev[-1]df = pl.DataFrame({"typed": ["aplpe", "bananna"], "correct": ["apple", "banana"]})df.with_columns( dist=ps.E(levenshtein, return_dtype=pl.Int64)(pl.col("typed"), pl.col("correct")))
Format columns into a string, the way str.format formats values.
template uses the same {field:spec} syntax as str.format (built on the same string.Formatter parser). Any field whose value is a pl.Expr is formatted per-row via Python’s own format(value, spec). A field whose value is a plain Python value (not a pl.Expr) is formatted once, immediately, like ordinary str.format.
templatestr
*args = ()
**kwargs = {}
Examples:
import polars as plimport polarstation as psdf = pl.DataFrame({"err": [0.5, 1.25, 12.0]})df.with_columns(msg=ps.format("error={:.2f}", pl.col("err")))
For several expressions in one template, ps.fmt_col(...) marks the spot inside a real f-string — the format spec (:.2f below) is written exactly where it would be for any other value:
Mark a column for embedding inside a real f-string, for a following ps.format(...).
Shorthand for FmtPlaceholder(pl.col(column)) when given a string, or FmtPlaceholder(column) directly when given a pl.Expr — see ps.format for the full explanation and examples.
columnIntoExpr
Examples:
import polars as plimport polarstation as psdf = pl.DataFrame({"err": [0.5, 1.25], "n": [3, 12]})df.with_columns( msg=ps.format(f"error={ps.fmt_col('err'):.2f} (n={ps.fmt_col('n')})"))
An expression that requires a LazyFrame context to resolve into a list of pl.Expr.
A plain pl.Expr is insufficient for operations like ps_enum.make() or ps_chop.chop() because Polars needs to know the output dtype (e.g. the exact pl.Enum([...]) category list) at plan-construction time — before any data is seen. FrameExpr defers that resolution to a two-phase execution model:
Phase 1 — peekps.with_columns calls resolve(lf) with the currentLazyFrame. The resolver runs a small aggregation (e.g. unique().sort() for category discovery, a handful of quantiles for binning) and collects it. Because the resolver receives the full lazy plan up to that point, any preceding .filter() or .select() calls are already embedded and Polars’ predicate/projection pushdown applies — only the relevant rows and columns are scanned.
Phase 2 — expression The resolver uses the aggregation result to construct a concrete pl.Expr with all dtype information baked in (e.g. pl.col("x").cast(pl.Enum(["a", "b", "c"]))). This expression is inserted back into the lazy plan and executed lazily together with all subsequent operations.