Skip to main content

polars-random

polars-random

PyPI version Python versions License: MIT CI Docs

Generate random numbers and statistical distributions natively in Polars DataFrames — a NumPy-style random API exposed as first-class Polars expressions, with reproducible seeds and per-row parameters.

polars-random is a Rust plugin offering four equivalent entry points so it composes naturally with the rest of polars:

Use case API
"Add a column of random draws to a DataFrame" df.random.<dist>(...)
Same thing, lazy lf.random.<dist>(...)
Inside any expression / with_columns / select pl.col("x").random.<dist>(...)  or  polars_random.<dist>(...)
Just give me N values as a Series polars_random.<dist>(..., size=N)
import polars as pl
import polars_random as pr  # registers DataFrame/LazyFrame/Expr namespaces

# 1. eager Series
pr.normal(mean=0.0, std=1.0, size=5, seed=42)

# 2. as a polars expression in any context
df = pl.DataFrame({"id": range(5)})
df.with_columns(noise=pr.normal(mean=0.0, std=1.0, seed=42))
df.with_columns(noise=pl.col("id").random.normal(seed=42))

# 3. as a DataFrame method (returns a new DataFrame with the column appended)
df.random.normal(mean=0.0, std=1.0, seed=42, name="noise")

# 4. inside a lazy pipeline
df.lazy().random.normal(seed=42, name="noise").collect()

Available distributions: rand / uniform, normal, binomial, randint. Every parameter (low, high, mean, std, n, p) accepts a Python scalar, a column name ("my_col"), or any pl.Expr. Nulls in column-valued parameters propagate as null in the output (no panic).

Why polars-random?

  • Polars-native — outputs are regular Polars columns, composable with the rest of your pipeline (no NumPy round-trips).
  • Per-row parametersmean, std, low, high, n, p can come from other columns, so each row can be drawn from a different distribution.
  • Reproducible — pass seed=... per call, or set one global seed with pr.set_random_seed(...).
  • Fast — implemented in Rust on top of rand / rand_distr.

Installation

uv add polars-random
poetry add polars-random
pip install polars-random

How it works (mental model)

Every distribution follows the same shape:

df.random.<distribution>(<params>, seed=None, name=None)
  • <params> are the distribution's parameters (e.g. low/high, mean/std, n/p).
  • Each parameter accepts a Python literal, a column name as a string, or a Polars expression (pl.col(...), arithmetic, etc.). Within a single call, all distribution parameters must be the same kind — either all literals or all expressions/column-names (no mixing).
  • seed makes the draw reproducible. Omit it for entropy-based randomness.
  • name is the new column's name. Defaults to the distribution name ("rand", "normal", "binomial").
  • The result is a new pl.DataFrame with the column appended. Calls chain.

Coming from NumPy?

NumPy polars-random
np.random.uniform(low, high, size=n) pr.rand(low=low, high=high, size=n)  or  df.random.rand(low=low, high=high)
np.random.normal(mean, std, size=n) pr.normal(mean=mean, std=std, size=n)
np.random.binomial(n, p, size=size) pr.binomial(n=n, p=p, size=size)
np.random.randint(low, high, size=n) pr.randint(low=low, high=high, size=n)
np.random.seed(42) (global) pr.set_random_seed(42) (global)  or  seed=42 per call
Different params per row (loop / vectorize manually) Pass a column name or pl.col(...) as the parameter

When used as a DataFrame/LazyFrame method or via the pl.col(...).random namespace, the output length is taken from the parent — no size= needed. Use size=N only with the top-level functions for "give me N values without a frame."

Seeding & reproducibility

Every draw takes an optional seed=. There are two ways to make results reproducible:

Per call — pass seed= to a single expression:

pr.normal(mean=0.0, std=1.0, seed=42)

Globally — set one seed once with pr.set_random_seed(...). Any draw that omits seed= then derives its seed from this global generator:

import polars as pl
import polars_random as pr

pr.set_random_seed(42)

df = pl.DataFrame({"id": range(5)})
df.with_columns(
    a=pr.normal(),               # reproducible — no per-call seed needed
    b=pr.rand(),                 # independent of `a`, also reproducible
)

This solves the "keep the distribution definition separate from where I set the seed" workflow: define length-free expressions (pr.normal(3.0, 1.0)) and let one global seed make the whole run reproducible, without threading seed=42 through every call.

Semantics:

  • Each expression consumes the global generator once, so distinct random columns in the same query stay independent (they are not byte-for-byte identical), just like NumPy's global RNG or polars' own set_random_seed.
  • Re-calling pr.set_random_seed(42) rewinds the sequence, so a script re-run reproduces the same draws.
  • An explicit seed= on a call always overrides the global seed for that call.
  • Without any global seed, seedless draws use OS entropy (the historical default) — nothing changes for existing code.

Want two identical columns?

Seedless draws under a global seed are always independent (that is the point), so they never coincide. To make two columns equal, give them the same explicit seed= — that pins both to the same draw and bypasses the global generator:

df.with_columns(
    a=pr.normal(seed=7),
    b=pr.normal(seed=7),   # a == b, byte-for-byte
)

Reproducibility depends on call order

Under a global seed, each seedless draw takes the next value from the generator, so reproducibility depends on the order and number of seedless draws — the same sequence of calls reproduces the same columns. Inserting or reordering a seedless draw shifts every later one (exactly like NumPy's or Polars' global RNG). If a specific column must stay fixed regardless of surrounding code, pin it with an explicit seed=.

pr.set_random_seed is independent of polars.set_random_seed. Polars seeds its own operations (.sample(), .shuffle(), …) and exposes only a setter — there is no public way for a plugin to read Polars' seed — so polars-random keeps its own global seed. Set both if you want Polars and polars-random reproducible in the same script.

Distributions

df.random.rand (uniform) · also aliased as df.random.uniform

Parameter Type Default Description
low float, str, pl.Expr, or None 0.0 Lower bound (inclusive).
high float, str, pl.Expr, or None 1.0 Upper bound (exclusive).
seed int or None None Reproducible draws.
name str or None "rand" Output column name.
import polars as pl
import polars_random

df = pl.DataFrame({
    "custom_low":  [0.0, 10.0, 100.0],
    "custom_high": [1.0, 20.0, 200.0],
})

(
    df
    # Scalar parameters
    .random.rand(low=1_000., high=2_000., seed=42, name="rand_scalar")
    # Default range [0, 1)
    .random.rand(seed=42, name="rand_default")
    # Per-row parameters via expression
    .random.rand(low=pl.col("custom_low"), high=pl.col("custom_high"), seed=42, name="rand_expr")
    # Per-row parameters via column name
    .random.rand(low="custom_low", high="custom_high", seed=42, name="rand_str")
)

df.random.normal

Parameter Type Default Description
mean float, str, pl.Expr, or None 0.0 Mean of the normal distribution.
std float, str, pl.Expr, or None 1.0 Standard deviation (must be > 0).
seed int or None None Reproducible draws.
name str or None "normal" Output column name.
import polars as pl
import polars_random

df = pl.DataFrame({
    "custom_mean": [0.0, 5.0, -3.0],
    "custom_std":  [1.0, 2.0, 0.5],
})

(
    df
    .random.normal(mean=3., std=2., seed=42, name="normal_scalar")
    .random.normal(seed=42, name="normal_default")  # mean=0, std=1
    .random.normal(mean=pl.col("custom_mean"), std=pl.col("custom_std"), seed=42, name="normal_expr")
    .random.normal(mean="custom_mean", std="custom_std", seed=42, name="normal_str")
)

df.random.binomial

Parameter Type Default Description
n int, str, or pl.Expr (required) Number of trials.
p float, str, or pl.Expr (required) Probability of success on each trial (0 ≤ p ≤ 1).
seed int or None None Reproducible draws.
name str or None "binomial" Output column name.
import polars as pl
import polars_random

df = pl.DataFrame({
    "n": [10, 50, 100],
    "p": [0.1, 0.5, 0.9],
})

(
    df
    .random.binomial(n=100, p=.5, seed=42, name="binomial_scalar")
    .random.binomial(n=pl.col("n"), p=pl.col("p"), seed=42, name="binomial_expr")
    .random.binomial(n="n", p="p", seed=42, name="binomial_str")
)

df.random.randint

Uniform random integers in [low, high) (high is exclusive, matching numpy.random.randint).

Parameter Type Default Description
low int, str, or pl.Expr 0 Lower bound (inclusive).
high int, str, or pl.Expr 2 Upper bound (exclusive).
seed int or None None Reproducible draws.
name str or None "randint" Output column name.
df.random.randint(low=0, high=10, seed=42)            # one column, scalar bounds
df.random.randint(low="lo", high="hi", seed=42)       # per-row bounds via columns

Beyond df.random — same kernel, four entry points

import polars as pl
import polars_random as pr

# 1. Top-level: returns a Series of N random values (NumPy-style).
pr.normal(mean=0, std=1, size=1_000, seed=42)

# 2. Top-level inside any expression (length comes from the surrounding context).
df.with_columns(noise=pr.normal(mean=0, std=1, seed=42))

# 3. Expression namespace — anchor random draws to an existing column.
df.with_columns(noise=pl.col("id").random.normal(mean=0, std=1, seed=42))

# 4. LazyFrame — keep random draws inside a lazy plan.
df.lazy().random.binomial(n=10, p=0.5, seed=42, name="trials").collect()

When a parameter is column-valued (pl.col(...), a column name, or any expression) and contains nulls, the output is null at those rows instead of raising.

Benchmarks

polars-random is a Rust plugin built on rand / rand_distr, so on the single-threaded path it matches numpy.random for the distributions where vectorisation dominates (uniform, normal, randint) and is sampler-bound for binomial. Run inside a LazyFrame with the streaming engine, polars parallelises the elementwise plugin across worker threads and pulls substantially further ahead.

Speedup is numpy_best_time / polars_random_best_time (best of 5×2 timed calls); a value above 1 means polars-random is faster. Full table and methodology: benchmarks/results.md.

Eager / lazy in-memory (single-threaded plugin)

df.with_columns(pr.<dist>(..., seed=42)) — the typical "add a random column" shape:

Distribution 10K rows 100K rows 1M rows 10M rows 50M rows
uniform 0.19x 0.77x 1.15x 1.78x 1.65x
normal 0.43x 1.43x 1.84x 1.96x 1.96x
randint 0.16x 0.52x 0.78x 1.30x 1.18x
binomial 0.65x 0.86x 0.89x 0.92x 0.91x

Lazy + streaming engine (parallel)

lf.with_columns(pr.<dist>(..., seed=42)).collect(engine="streaming"):

Distribution 10K rows 100K rows 1M rows 10M rows 50M rows
uniform 0.07x 0.48x 1.39x 3.85x 3.66x
normal 0.18x 1.38x 2.89x 5.31x 5.40x
randint 0.07x 0.30x 1.39x 2.95x 3.30x
binomial 0.69x 2.05x 3.24x 3.48x 3.39x

At small sizes the polars expression engine pays a fixed per-call cost (a few hundred microseconds) so numpy wins; from ~1M rows raw kernel speed dominates. On the streaming engine, binomial — sampler-bound on a single thread — is the biggest winner because the heavy per-row work parallelises cleanly.

Two caveats:

  1. Streaming re-seeds per chunk. The streaming engine processes data in chunks and the plugin is invoked once per chunk with the same seed=, so the resulting column is deterministic for a given chunking but differs bit-for-bit from the in-memory engine. Both are valid samples from the distribution; pick the engine first, then fix seed.
  2. NumPy → Polars also costs nothing extra at scale — the numpy -> pl.Series row in benchmarks/results.md is within ~1% of plain numpy. The polars-random win is in the kernel itself plus parallelism, not in avoiding a copy.

Reproducing

# release build of the Rust extension
just install-release

# run the benchmark (writes benchmarks/results.md and benchmarks/results.json)
just bench

just bench is equivalent to:

uv run --with numpy python benchmarks/benchmark.py

The script accepts --sizes, --repeats, --inner, --output, and --json flags; see python benchmarks/benchmark.py --help. Defaults are --sizes 10000,100000,1000000,10000000,50000000 --repeats 5 --inner 2.

Documentation

Full API reference: https://diegoglozano.github.io/polars-random/

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polars_random-0.5.0.tar.gz (132.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

polars_random-0.5.0-cp39-abi3-win_amd64.whl (4.4 MB view details)

Uploaded CPython 3.9+Windows x86-64

polars_random-0.5.0-cp39-abi3-win32.whl (3.8 MB view details)

Uploaded CPython 3.9+Windows x86

polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl (5.8 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ppc64le

polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl (5.7 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ i686

polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl (5.1 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARMv7l

polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (5.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file polars_random-0.5.0.tar.gz.

File metadata

  • Download URL: polars_random-0.5.0.tar.gz
  • Upload date:
  • Size: 132.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: maturin/1.14.1

File hashes

Hashes for polars_random-0.5.0.tar.gz
Algorithm Hash digest
SHA256 79740290f2b5a73859182deb7f15baaa0aac553281f2fea19bf245f9cdb14243
MD5 6c6bb8b5e2da49405ba210fabcc03128
BLAKE2b-256 f78a084903dab20f1c983cfe031887b0db420143135cd7a21431f36902ccffcc

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 7cbd11a2becd455dd8c1c7e5114232a016ad75d8183f67df9b734cb588a89853
MD5 7306abec8906602025ae308f9f83a9f1
BLAKE2b-256 c64f806b889a825e7c939fe7a4d1c115d5c4ee74bc1a065417cb7122eb570af5

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-win32.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-win32.whl
Algorithm Hash digest
SHA256 cfa6bbf79ca0e3b1788dfc1e8620fd951b599cc1edd754fedbd983142bca6a00
MD5 24de84c851a57aa3c29cb9e3bbeb7b5f
BLAKE2b-256 0a11db5f6ad7c4e45aad4f34a9abee16e84252542a7efeb959e0319bd0af57bc

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1c2dba570e293c637616fd3f3bd95f41673878ec84ed4fb1a5f22c8a6218fda0
MD5 40ee8c5aaaa1ff967b25698dc35fa33c
BLAKE2b-256 c5fde471be42b71c0a0b2df7c05b21a30968ca17d154d3228645c0d08e17826f

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl
Algorithm Hash digest
SHA256 cd06eaac1966c84c7d5163eac357de6c62d7f2178b0d0d4a845295d2f0422b7a
MD5 88641c969f641582310acaba0d1a12ef
BLAKE2b-256 cf4829397e96246a7ad3f949e165bf944a2f0c44ad0c0e2873d2f3323d475d3b

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 9220ec56ffedc081574960725cd307530eec5cd1f17b83228e80ac421689984c
MD5 3abe2f5f3caa99e713e3316372c851a9
BLAKE2b-256 0334660b4504cae3b43d4a08778245593c724ed0f06fd27a75d159a38e73a659

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Algorithm Hash digest
SHA256 068c4082988fddd2ee64d130ad42b47f6e5d521e04130f22bc3dfd7e29540c92
MD5 7c5815a753d30dfcbc4cdd5ba4942d4b
BLAKE2b-256 5834446d68202e3bf918f2c4f719834a571b704310e73413968e5ea51e6c3466

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 644beeeffbef5d81db90f1b1f8b9d34475ec19d26a961e4c28a25798fe6df940
MD5 c60adb1cb966a058000dbcea8554e712
BLAKE2b-256 7a18c36402211fc4dc89d8a4b7f7077ec415b32a7b50d915540980e31600c960

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 aa89354872b96602ec73f25e6197b136c249c4cacb2ff12c3e19e80c1524fded
MD5 6a85f11f9a5ba417a9e5c26127fc6905
BLAKE2b-256 af04eb4e2397a17e1065fce8210fa327868a94a47bc09ebbf4fb8e4119b98dd1

See more details on using hashes here.

File details

Details for the file polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4d48ba8d0bcbfa95debe32f95f158c2075c00f7f7cdce998a2d9bbc8d00f9f77
MD5 bfb89ce4cf22dceefae1260961265083
BLAKE2b-256 7fb8249bc8e311a9f73a3e2aeb601deb7e4ffaf63fb78b40227a1405f8681d28

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page