polars-random
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 parameters —
mean,std,low,high,n,pcan come from other columns, so each row can be drawn from a different distribution. - Reproducible — pass
seed=...per call, or set one global seed withpr.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). seedmakes the draw reproducible. Omit it for entropy-based randomness.nameis the new column's name. Defaults to the distribution name ("rand","normal","binomial").- The result is a new
pl.DataFramewith 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' ownset_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_seedis independent ofpolars.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 — sopolars-randomkeeps its own global seed. Set both if you want Polars andpolars-randomreproducible 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:
- 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 fixseed. - NumPy → Polars also costs nothing extra at scale — the
numpy -> pl.Seriesrow inbenchmarks/results.mdis 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
Metadata
Release files for polars-random 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| polars_random-0.5.0.tar.gz | 132.6 kB | Details |
Built distributions (wheels)
Total release size: 43.2 MB
Release files / polars_random-0.5.0.tar.gz
| Download URL | polars_random-0.5.0.tar.gz |
|---|---|
| Size | 132.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
79740290f2b5a73859182deb7f15baaa0aac553281f2fea19bf245f9cdb14243
|
|
BLAKE2b-256 checksum How to use checksums |
f78a084903dab20f1c983cfe031887b0db420143135cd7a21431f36902ccffcc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-win_amd64.whl
| Download URL | polars_random-0.5.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
7cbd11a2becd455dd8c1c7e5114232a016ad75d8183f67df9b734cb588a89853
|
|
BLAKE2b-256 checksum How to use checksums |
c64f806b889a825e7c939fe7a4d1c115d5c4ee74bc1a065417cb7122eb570af5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-win32.whl
| Download URL | polars_random-0.5.0-cp39-abi3-win32.whl |
|---|---|
| Size | 3.8 MB |
| Tags | CPython 3.9 Windows x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
cfa6bbf79ca0e3b1788dfc1e8620fd951b599cc1edd754fedbd983142bca6a00
|
|
BLAKE2b-256 checksum How to use checksums |
0a11db5f6ad7c4e45aad4f34a9abee16e84252542a7efeb959e0319bd0af57bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 5.0 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1c2dba570e293c637616fd3f3bd95f41673878ec84ed4fb1a5f22c8a6218fda0
|
|
BLAKE2b-256 checksum How to use checksums |
c5fde471be42b71c0a0b2df7c05b21a30968ca17d154d3228645c0d08e17826f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl
| Download URL | polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl |
|---|---|
| Size | 5.8 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ PowerPC 64-le abi3 |
|
SHA-256 checksum How to use checksums |
cd06eaac1966c84c7d5163eac357de6c62d7f2178b0d0d4a845295d2f0422b7a
|
|
BLAKE2b-256 checksum How to use checksums |
cf4829397e96246a7ad3f949e165bf944a2f0c44ad0c0e2873d2f3323d475d3b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl
| Download URL | polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl |
|---|---|
| Size | 5.7 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
9220ec56ffedc081574960725cd307530eec5cd1f17b83228e80ac421689984c
|
|
BLAKE2b-256 checksum How to use checksums |
0334660b4504cae3b43d4a08778245593c724ed0f06fd27a75d159a38e73a659
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
| Download URL | polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl |
|---|---|
| Size | 5.1 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARMv7l abi3 |
|
SHA-256 checksum How to use checksums |
068c4082988fddd2ee64d130ad42b47f6e5d521e04130f22bc3dfd7e29540c92
|
|
BLAKE2b-256 checksum How to use checksums |
5834446d68202e3bf918f2c4f719834a571b704310e73413968e5ea51e6c3466
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 5.0 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
644beeeffbef5d81db90f1b1f8b9d34475ec19d26a961e4c28a25798fe6df940
|
|
BLAKE2b-256 checksum How to use checksums |
7a18c36402211fc4dc89d8a4b7f7077ec415b32a7b50d915540980e31600c960
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
aa89354872b96602ec73f25e6197b136c249c4cacb2ff12c3e19e80c1524fded
|
|
BLAKE2b-256 checksum How to use checksums |
af04eb4e2397a17e1065fce8210fa327868a94a47bc09ebbf4fb8e4119b98dd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|
Release files / polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
4d48ba8d0bcbfa95debe32f95f158c2075c00f7f7cdce998a2d9bbc8d00f9f77
|
|
BLAKE2b-256 checksum How to use checksums |
7fb8249bc8e311a9f73a3e2aeb601deb7e4ffaf63fb78b40227a1405f8681d28
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.14.1
|