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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79740290f2b5a73859182deb7f15baaa0aac553281f2fea19bf245f9cdb14243
|
|
| MD5 |
6c6bb8b5e2da49405ba210fabcc03128
|
|
| BLAKE2b-256 |
f78a084903dab20f1c983cfe031887b0db420143135cd7a21431f36902ccffcc
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 4.4 MB
- Tags: CPython 3.9+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cbd11a2becd455dd8c1c7e5114232a016ad75d8183f67df9b734cb588a89853
|
|
| MD5 |
7306abec8906602025ae308f9f83a9f1
|
|
| BLAKE2b-256 |
c64f806b889a825e7c939fe7a4d1c115d5c4ee74bc1a065417cb7122eb570af5
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-win32.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-win32.whl
- Upload date:
- Size: 3.8 MB
- Tags: CPython 3.9+, Windows x86
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfa6bbf79ca0e3b1788dfc1e8620fd951b599cc1edd754fedbd983142bca6a00
|
|
| MD5 |
24de84c851a57aa3c29cb9e3bbeb7b5f
|
|
| BLAKE2b-256 |
0a11db5f6ad7c4e45aad4f34a9abee16e84252542a7efeb959e0319bd0af57bc
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 5.0 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c2dba570e293c637616fd3f3bd95f41673878ec84ed4fb1a5f22c8a6218fda0
|
|
| MD5 |
40ee8c5aaaa1ff967b25698dc35fa33c
|
|
| BLAKE2b-256 |
c5fde471be42b71c0a0b2df7c05b21a30968ca17d154d3228645c0d08e17826f
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl
- Upload date:
- Size: 5.8 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ppc64le
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd06eaac1966c84c7d5163eac357de6c62d7f2178b0d0d4a845295d2f0422b7a
|
|
| MD5 |
88641c969f641582310acaba0d1a12ef
|
|
| BLAKE2b-256 |
cf4829397e96246a7ad3f949e165bf944a2f0c44ad0c0e2873d2f3323d475d3b
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl
- Upload date:
- Size: 5.7 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ i686
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9220ec56ffedc081574960725cd307530eec5cd1f17b83228e80ac421689984c
|
|
| MD5 |
3abe2f5f3caa99e713e3316372c851a9
|
|
| BLAKE2b-256 |
0334660b4504cae3b43d4a08778245593c724ed0f06fd27a75d159a38e73a659
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
- Upload date:
- Size: 5.1 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARMv7l
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
068c4082988fddd2ee64d130ad42b47f6e5d521e04130f22bc3dfd7e29540c92
|
|
| MD5 |
7c5815a753d30dfcbc4cdd5ba4942d4b
|
|
| BLAKE2b-256 |
5834446d68202e3bf918f2c4f719834a571b704310e73413968e5ea51e6c3466
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 5.0 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
644beeeffbef5d81db90f1b1f8b9d34475ec19d26a961e4c28a25798fe6df940
|
|
| MD5 |
c60adb1cb966a058000dbcea8554e712
|
|
| BLAKE2b-256 |
7a18c36402211fc4dc89d8a4b7f7077ec415b32a7b50d915540980e31600c960
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 4.1 MB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa89354872b96602ec73f25e6197b136c249c4cacb2ff12c3e19e80c1524fded
|
|
| MD5 |
6a85f11f9a5ba417a9e5c26127fc6905
|
|
| BLAKE2b-256 |
af04eb4e2397a17e1065fce8210fa327868a94a47bc09ebbf4fb8e4119b98dd1
|
File details
Details for the file polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: polars_random-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 4.3 MB
- Tags: CPython 3.9+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d48ba8d0bcbfa95debe32f95f158c2075c00f7f7cdce998a2d9bbc8d00f9f77
|
|
| MD5 |
bfb89ce4cf22dceefae1260961265083
|
|
| BLAKE2b-256 |
7fb8249bc8e311a9f73a3e2aeb601deb7e4ffaf63fb78b40227a1405f8681d28
|