Skip to main content

Polars List Utils (polist)

polist is a Python package that provides a set of utilities for working with List-type columns in Polars DataFrames, especially for signal processing and feature extraction.

So far these utilities comprise those that I found to be missing or lacking from the List namespace within the Polars library while I was working on a project at work that required extensive handling of signal data which I was storing in Polars DataFrames.

By providing these utilities as a Polars plugin, and thus not having to leave the Polars DataFrame for these operations, I was able to significantly speed up the processing of my data by benefiting from Polars query optimization and parallelization. So while the operations themselves are not necessarily faster than their Numpy counterparts (although they might be in some cases), the integration with Polars gave my larger processing pipeline a significant speed boost.

A disclaimer about authorship

This library started life as a hand-crafted solution to a problem at work. The time pressures of life and work eventually led me to succumb to taking shortcuts and let AI take the wheel. This repo is therefore no longer "artisanal": it is AI-generated, reviewed to the best of my time and abilities, and fully dog-fooded by me at work.

Features

  • polist.apply_interp

    • Interpolates a new List-type column from 3 specified List-type columns.
    • Behaviour as expected from the numpy.interp function, but for Polars DataFrames.
    • Supply the x_column, y_column, and xp_column columns to obtain the interpolated y-values.
  • polist.apply_butterworth

    • Applies a Butterworth filter (low-pass, high-pass, band-pass) to a List-type column of signal data.
    • The filter is applied bidirectionally (like scipy.signal.filtfilt), so there is no phase lag, but the magnitude response is squared: the cutoff sits at -6dB instead of -3dB.
    • A band-pass doubles the design order, as in scipy.signal.butter.
  • polist.apply_fft

    • Applies a (real) Fast Fourier Transform (FFT) to a List-type column of signal data, producing the N/2 + 1 amplitudes from DC to Nyquist.
    • Can pre-process the signals with a windowing function (e.g. Hann, Blackman), applied in periodic (sym=False) form as in scipy.signal.periodogram.
    • Scales the amplitudes under scipy's names: amplitude reads a tone's peak amplitude A, amplitude_squared reads A² (the square of amplitude, a convenience), spectrum its mean-square A²/2 (scipy.signal.periodogram(scaling="spectrum")), and density its power density (scaling="density", integrates to the signal's mean-square power).
    • The length of each signal must be a power of two of at least 2 — a violation raises — and the corresponding frequency axis is [i * sample_rate / N for i in range(N // 2 + 1)].
  • polist.agg_slices

    • Computes an aggregation of a range of y-values defined by some x-values for List-type columns.
    • This is useful for feature extraction from signals, e.g. to compute the mean of a signal in a certain time range or a spectrum in a certain frequency range.
    • Ranges to include or exclude can be given as simple (lo, hi) tuples or with explicit boundary modes like ((lo, "closed"), (hi, "open")).
  • polist.agg_lists

    • Applies element-wise list-aggregations to a List-type column in a GroupBy context.
    • Composed from Polars' own vertical aggregations, with a small pass-through plugin that normalizes Array to List and carries the strict/list_length checks; it lives here so that both aggregation functions offer the same aggregations with the same missing-data behaviour. One consequence: it aggregates in the column's own dtype, as Polars does, rather than in f64 -- so near the Float32 range limit it can overflow where the round-once plugin kernels stay finite.
  • polist.zip_binary

    • Applies a binary operation element-wise between two List-type columns, per element exactly as the corresponding scalar Polars operation: arithmetic (add, sub, mul, div) with null propagation and IEEE float behaviour (output dtype follows Polars supertyping, so two Float32 inputs stay Float32), and/or on Boolean lists with Kleene logic, and comparisons (gt, ge, lt, le, eq, ne) following Polars' total order (NaN equals NaN and exceeds everything else), emitting Boolean lists.
  • polist.cum_agg_runs

    • Cumulatively aggregates a List-type column within runs gated by a Boolean List-type column: a run is a maximal region of constant gate value (null gates break runs), only True-gated elements accumulate, and every other position emits null (or a plain zero with outside="zero").
    • Each emitted element equals the vertical aggregation over the run's elements so far, so an all-True gate over Float64 tracks cum_sum/cum_min/cum_max/cum_count; null values emit null but keep the running state, except count, which emits the running count as cum_count does.
    • This function has always emitted the true prefix extreme -- NaN for an all-NaN prefix, or a leading infinity -- where Polars up to 1.43 leaked its cum_min/ cum_max fold seed (+f64::MAX for cum_min, -f64::MAX for cum_max; briefly ±inf) at those positions. Polars >= 1.44 agrees with this function. Float32 values accumulate in f64 and round once at the boundary, so a Polars version that accumulates step-wise in Float32 can differ in the last bits and near the range limit.
    • Float32 values stay Float32; count emits UInt32 (Polars' count dtype).

Every function accepts a length-1 literal list column (e.g. pl.lit([...])) for any input and broadcasts it across rows, provided at least one other input is a real column -- as in Polars generally, an expression built only from length-1 literals produces a single row rather than broadcasting, so the claim is moot for the single-input transforms and for agg_lists, which aggregates rows rather than mapping them. Anything structural raises instead of silently returning nulls: invalid configuration (bad cutoffs, unknown window names), mismatched paired lengths, integer inner dtypes, and per-row shapes a transform cannot process — a non-power-of-two FFT length, a Butterworth signal shorter than its reflection padding, an empty list. For mixed-length List data the escape hatch is pre-filtering with list.len() before the transform — in a lazy query the optimizer pushes a later filter below the transform, so offending rows are dropped before validation rather than raising. Only genuinely missing data nulls: a null row, or a null element inside a transform's signal.

Float32 columns stay Float32; the plugin kernels compute in f64 and round once at the output boundary, while agg_lists aggregates in the column's own dtype as Polars does. The one dtype exception: count emits UInt32, Polars' count dtype, everywhere it appears.

Both aggregation functions support sum, mean, median, std, min, max, delta, and count, and both handle missing data like the vertical aggregations of Polars itself. (Semantically, that is: floating-point accumulation order differs from Polars', so results agree to rounding rather than bit-for-bit.)

  • Nulls are skipped: excluded from both the numerator and the denominator, so count only counts non-null values and an empty or all-null selection yields null, count reading 0 and sum following the empty setting below.
  • NaNs propagate: Polars treats NaN as a legitimate float value, so it flows into the sum and poisons sum, mean and std. Two exceptions to be aware of are min and max, which skip NaNs (unless all values are NaN), and median sorts NaN as the largest value.
  • std is the sample standard deviation (ddof=1) and yields null for fewer than two values, as in Polars.

The three transforms instead follow a three-line rule: missing data nulls (a null row or a null element yields a null row — a signal with missing samples cannot be meaningfully transformed); invalid values propagate (NaN and ±inf flow through the arithmetic — one NaN yields an all-NaN spectrum, staying visibly invalid rather than becoming missing); wrong shape raises. For the aggregations an empty list is simply an empty selection, and zip_binary and cum_agg_runs mirror the corresponding scalar and cumulative Polars operations per element, treating empty lists as valid (empty in, empty out).

List and Array

Every function accepts both List and Array columns. A length-preserving function returns the container it was given; apply_fft narrows an Array(w) to Array(w // 2 + 1), and apply_interp follows its xp argument, since that decides the output's shape. zip_binary returns an Array only when both operands are Arrays, since both are operands; cum_agg_runs follows its value column alone, its gate being a mask rather than an operand. Where two inputs must agree element-for-element, their widths are checked before execution if both are Arrays, and per row otherwise. Two exceptions to container propagation: agg_slices reduces to a scalar, and agg_lists always returns a List, being an expression composition that never sees its input's schema (an Array input to it must match list_length exactly, rather than being null-padded like a short List).

Zero-width Arrays are rejected at any nesting depth, because Polars panics when slicing them; zero-length Lists are fine.

Two conventions, one switch

Polars answers "what is the sum of no values" with 0.0, the identity element of addition. For feature extraction that is the dangerous answer: an empty selection usually means a misconfigured range, and a real 0.0 disguises that as a measurement which then sails through a threshold. So agg_slices and agg_lists take an empty argument:

  • empty="null" (default) — summing nothing is unknown, not zero.
  • empty="zero" — Polars' convention, matching explode().group_by().agg(col.filter(...).sum()).

It reaches exactly one cell of the matrix: sum is the only aggregation with a non-null identity, every other one yields null over nothing either way, and count yields 0 either way.

The switch governs an empty selection — no index fell in range, or the lists were themselves empty, the limiting case of the same condition. A null row stays null under both settings for every aggregation, count included. So empty="zero" is not the same as .fill_null(0.0): that cannot tell a missing row from an empty selection, and would turn a missing measurement into a real zero, which is the error this flag exists to avoid.

The same philosophy sets the strict default. apply_interp verifies that x is non-decreasing, agg_slices rejects inverted or NaN range bounds at expression construction, and agg_lists raises on lists carrying non-null data past list_length rather than silently truncating it (a null-padded tail is lossless and never raises) — all on by default, since each covers a failure that is otherwise silent. Pass strict=False for the lenient readings (numpy's unsorted-x behaviour, empty selections from inverted ranges, list_length as a deliberate window). NaN, which announces itself in the output, propagates rather than raising either way.

Errors raised from inside a row describe the offending row by its shape rather than its position: Polars hands a plugin one chunk at a time, so any row number counted there would be chunk-local and would point at a different, valid row of the frame.

Example

import polars as pl
import polars_list_utils as polist

Fs, N = 200.0, 1024
FREQS = [i * Fs / N for i in range(N // 2 + 1)]

df = (
    df
    .with_columns(
        polist.apply_butterworth(
            "signal",
            sample_rate=Fs,
            max_freq=50.0,
        )
        .alias("filtered")
    )
    .with_columns(
        polist.apply_fft(
            "filtered",
            sample_rate=Fs,
            window="hann",
            scaling="amplitude",
        )
        .alias("fft")
    )
    .with_columns(
        polist.agg_slices(
            "fft",
            pl.lit(FREQS),
            aggregation="mean",
            slices_include=[(20.0, 30.0)],
        )
        .alias("mean_20_30hz")
    )
)

See examples/showcase.py for the full pipeline:

Showcase

Installation (user)

uv pip install polars-list-utils

Installation (developer)

  1. Setup Rust (i.e. install rustup)
  2. Setup Python (i.e. install uv)
  3. Setup environment and compile plugin:
uv sync --extra dev
uv run maturin develop --release --uv
  1. (Maybe) configure Cargo to find uv's Python installs. For example:
# .cargo/config.toml
[env]
PYO3_PYTHON = "C:\\Users\\travis.hammond\\AppData\\Roaming\\uv\\python\\cpython-3.12.0-windows-x86_64-none\\python.exe"
  1. Test and run the example:
uv run pytest
uv run ./examples/showcase.py
  1. Lint:
uvx ty check
uvx ruff check
cargo clippy --release -- -D warnings
cargo fmt

Changelog

2.0.0

One bundled breaking release. New: the sum aggregation everywhere; zip_binary (element-wise arithmetic, Kleene and/or, and total-ordered comparisons between two list columns); cum_agg_runs (cumulative aggregation within gate-delimited runs, matching Polars' cum_* semantics); Array columns accepted by every function, with the container preserved; and an empty switch on the aggregations choosing what the sum of nothing is -- "null" (default) or Polars' "zero".

Breaking: strict checks are on by default (unsorted interpolation axes, invalid ranges, and lossy agg_lists truncation now raise; pass strict=False for the old leniency). Wrong shape raises instead of yielding null rows (non-power-of-two FFT lengths, too-short Butterworth signals, empty lists into transforms -- pre-filter with list.len() upstream), while NaN/±inf now propagate into visibly invalid output instead of becoming null: guard thresholds with is_finite(). Float32 columns stay Float32, count returns UInt32, and integer inner dtypes raise. FFT scalings use scipy's names -- power -> spectrum, psd -> density, plus new amplitude_squared; window names are unchanged. The even-count median now matches Polars exactly (last-ulp changes on stored features).

Fixed: Array inputs no longer abort the process; no input dtype can panic; aliased or JSON-shaped range arguments no longer crash agg_slices.

Metadata

Release files for polars-list-utils 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for polars-list-utils 2.0.0
File Size Uploaded
polars_list_utils-2.0.0.tar.gz 373.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for polars-list-utils 2.0.0
File
polars_list_utils-2.0.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
polars_list_utils-2.0.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
polars_list_utils-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 32.0 MB

Release files / polars_list_utils-2.0.0.tar.gz

Download URL polars_list_utils-2.0.0.tar.gz
Size 373.6 kB
Tags Source
SHA-256 checksum
How to use checksums
0058c10d170704b9f80dae6aaa312721afcff45573533e21e9623fe0db55c993
BLAKE2b-256 checksum
How to use checksums
113cae1d712ad6bc92179ce157578c4b0fb50e896dbe4660b78b24d436bf4296
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / polars_list_utils-2.0.0-cp310-abi3-win_amd64.whl

Download URL polars_list_utils-2.0.0-cp310-abi3-win_amd64.whl
Size 6.2 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
37ff66394154de9fa8b2295234bf4abf464986596a5118b16d49a4a02c3e50e9
BLAKE2b-256 checksum
How to use checksums
5f21f4c0663034c7857812c0918bcde6c96b7dea1216228c67a535acb174760f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 6.9 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
f1a03052032e7a8f02e3b5ced8fd370052bed02c9eba62ac611066dd912e00e7
BLAKE2b-256 checksum
How to use checksums
d470e06bdbe59b788e4c458f362ba6290f2f2f26de70324daf0e0660dc39ec61
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL polars_list_utils-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 6.8 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
fd0ba248c5e1a51418fec4631d15e314b1792da6e3bb0900dfd8394024c99134
BLAKE2b-256 checksum
How to use checksums
b91f500220aeef55b143f4f1fe4c4e236d12afb8a81a0afb5a4ceff9651ccd44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / polars_list_utils-2.0.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL polars_list_utils-2.0.0-cp310-abi3-macosx_11_0_arm64.whl
Size 5.6 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b33759b132d9dd1bf2197a952db385a3f311a455c76184f7c36fde4f2596f2ab
BLAKE2b-256 checksum
How to use checksums
b6c74fb3662539b9e2387b515f2a4b79a5a6f362aa10302c624ac182b136bf50
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / polars_list_utils-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL polars_list_utils-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 6.0 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
c88b29aa7611a53a52e3fdf550a0e5b47a62a3000e990c3ba1b9954f4d8af9fd
BLAKE2b-256 checksum
How to use checksums
01a63e19e0978eb71f9a4674c8e0d225cc0158d495e59113bb126aa015042c3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release history Release notifications | RSS feed

This release

2.0.0 This release

6 release files

1.0.0

6 release files

0.9.1

13 release files

0.9.0

13 release files

0.1.0

14 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page