Skip to content

Continuous distributions

Every continuous distribution inherits the shared method surface from ContinuousDistribution (shown first); the concrete classes follow, alphabetically. The members listed under each class include those inherited methods.

Base class

ContinuousDistribution

Abstract base class for continuous univariate distributions.

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

mean abstractmethod

mean() -> Expr

Expected value E[X].

Source code in polars_stats/distributions/_base.py
@abstractmethod
def mean(self) -> pl.Expr:
    """Expected value `E[X]`."""

variance abstractmethod

variance() -> Expr

Variance Var[X] = E[(X - E[X])^2].

Source code in polars_stats/distributions/_base.py
@abstractmethod
def variance(self) -> pl.Expr:
    """Variance `Var[X] = E[(X - E[X])^2]`."""

std

std() -> Expr

Standard deviation, sqrt(variance).

Source code in polars_stats/distributions/_base.py
def std(self) -> pl.Expr:
    """Standard deviation, `sqrt(variance)`."""
    return self.variance().sqrt()

median

median() -> Expr

Median, ppf(0.5).

Source code in polars_stats/distributions/_base.py
def median(self) -> pl.Expr:
    """Median, `ppf(0.5)`."""
    return self.ppf(0.5)

entropy abstractmethod

entropy() -> Expr

Differential or Shannon entropy, in nats.

Source code in polars_stats/distributions/_base.py
@abstractmethod
def entropy(self) -> pl.Expr:
    """Differential or Shannon entropy, in nats."""

Distributions

Beta

Beta(a: float | IntoExprColumn, b: float | IntoExprColumn)

Bases: ContinuousDistribution

Beta distribution on [0, 1] with shape parameters a (alpha) and b (beta).

Equivalent to scipy.stats.beta(a, b). The parameter names follow scipy; statrs calls them shape_a / shape_b.

Parameters:

Name Type Description Default
a float | IntoExprColumn

First shape parameter (alpha), with a > 0. Either a Python float or an IntoExprColumn (pl.Expr, pl.Series or column name str) carrying one shape per row.

required
b float | IntoExprColumn

Second shape parameter (beta), with b > 0. Same accepted types as a.

required

An invalid shape (a <= 0, b <= 0, or a non-finite parameter) is not checked at construction; matching every other distribution, it raises InvalidOperation (a ComputeError) when any method is evaluated. Null parameters propagate to null.

The support is [0, 1]: pdf is 0 outside it, and when a shape is < 1 the density diverges (inf or large finite values) at the corresponding boundary.

Source code in polars_stats/distributions/_beta.py
def __init__(self, a: float | IntoExprColumn, b: float | IntoExprColumn) -> None:
    self._a = coerce_param(a, name="a")
    self._b = coerce_param(b, name="b")
    self._scalar_kwargs = scalar_kwargs(a=scalar_float(a), b=scalar_float(b))

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

std

std() -> Expr

Standard deviation, sqrt(variance).

Source code in polars_stats/distributions/_base.py
def std(self) -> pl.Expr:
    """Standard deviation, `sqrt(variance)`."""
    return self.variance().sqrt()

median

median() -> Expr

Median, ppf(0.5).

Source code in polars_stats/distributions/_base.py
def median(self) -> pl.Expr:
    """Median, `ppf(0.5)`."""
    return self.ppf(0.5)

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

mean

mean() -> Expr

Expected value, a / (a + b).

Source code in polars_stats/distributions/_beta.py
def mean(self) -> pl.Expr:
    """Expected value, ``a / (a + b)``."""
    return self._moment(self._a / (self._a + self._b))

variance

variance() -> Expr

Variance, a * b / ((a + b)^2 * (a + b + 1)).

Source code in polars_stats/distributions/_beta.py
def variance(self) -> pl.Expr:
    """Variance, ``a * b / ((a + b)^2 * (a + b + 1))``."""
    return self._moment(self._a * self._b / ((self._a + self._b) ** 2 * (self._a + self._b + 1)))

entropy

entropy() -> Expr

Differential entropy in nats, ln B(a, b) - (a - 1) psi(a) - (b - 1) psi(b) + (a + b - 2) psi(a + b).

Source code in polars_stats/distributions/_beta.py
def entropy(self) -> pl.Expr:
    """Differential entropy in nats, ``ln B(a, b) - (a - 1) psi(a) - (b - 1) psi(b) + (a + b - 2) psi(a + b)``."""
    # Unlike ``mean`` / ``variance`` there is no elementary closed form (log-Beta and digamma), so the
    # formula is evaluated by ``beta_entropy`` in Rust. For column parameters that runs once per row; for
    # scalar parameters it is computed **once** on length-1 inputs and broadcast to length-n behind the
    # ``_moment`` validity gate, so a constant's entropy is not re-evaluated on every row.
    if self._scalar_kwargs is None:
        return register_plugin("beta_entropy", (self._a, self._b))
    return self._moment(register_plugin("beta_entropy", self._scalar_lit_args()))

Exponential

Exponential(rate: float | IntoExprColumn)

Bases: ContinuousDistribution

Exponential distribution with rate rate (λ).

Equivalent to scipy.stats.expon(scale=1 / rate). The API exposes rate (the statrs parameterisation) rather than scipy's scale = 1 / rate: it is the natural parameter and avoids the divide-by-zero footgun of passing scale=0.

Parameters:

Name Type Description Default
rate float | IntoExprColumn

Rate parameter λ, with rate > 0. Either a Python float or an IntoExprColumn (pl.Expr, pl.Series or column name str) carrying one rate per row.

required

An invalid rate (rate <= 0 or NaN) is not checked at construction; matching every other distribution, it raises InvalidOperation (a ComputeError) when any method is evaluated. A null rate propagates to null. The support is x >= 0: pdf and cdf are 0 for x < 0, and sf is 1 there.

Source code in polars_stats/distributions/_exponential.py
def __init__(self, rate: float | IntoExprColumn) -> None:
    self._rate = coerce_param(rate, name="rate")
    self._scalar_kwargs = scalar_kwargs(rate=scalar_float(rate))

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

mean

mean() -> Expr

Expected value, 1 / rate.

Source code in polars_stats/distributions/_exponential.py
def mean(self) -> pl.Expr:
    """Expected value, ``1 / rate``."""
    return 1 / self._checked_rate

variance

variance() -> Expr

Variance, 1 / rate**2.

Source code in polars_stats/distributions/_exponential.py
def variance(self) -> pl.Expr:
    """Variance, ``1 / rate**2``."""
    return 1 / self._checked_rate**2

std

std() -> Expr

Standard deviation, 1 / rate, the same expression as mean.

Overrides the base-class variance().sqrt(), which squares the rate and then unsquares it: the round trip saturates about 300 decades before 1 / rate does.

Source code in polars_stats/distributions/_exponential.py
def std(self) -> pl.Expr:
    """Standard deviation, ``1 / rate``, the same expression as ``mean``.

    Overrides the base-class ``variance().sqrt()``, which squares the rate and then unsquares
    it: the round trip saturates about 300 decades before ``1 / rate`` does.
    """
    return 1 / self._checked_rate

median

median() -> Expr

Median, log(2) / rate.

Source code in polars_stats/distributions/_exponential.py
def median(self) -> pl.Expr:
    """Median, ``log(2) / rate``."""
    return math.log(2) / self._checked_rate

entropy

entropy() -> Expr

Differential entropy, 1 - log(rate).

Source code in polars_stats/distributions/_exponential.py
def entropy(self) -> pl.Expr:
    """Differential entropy, ``1 - log(rate)``."""
    return 1 - self._checked_rate.log()

LogNormal

LogNormal(mu: float | IntoExprColumn = 0.0, sigma: float | IntoExprColumn = 1.0)

Bases: ContinuousDistribution

Log-normal distribution: X such that ln(X) is Normal(mu, sigma).

Parameterised by the underlying normal's location mu and scale sigma (sigma > 0).

Equivalent to scipy.stats.lognorm(s=sigma, scale=exp(mu)) (with loc=0): scipy's shape s is sigma and its scale is exp(mu).

The support is x > 0; pdf and cdf are 0 and sf is 1 for x <= 0, matching scipy.

Parameters:

Name Type Description Default
mu float | IntoExprColumn

Location of the underlying normal (mean of ln(X)). Either a Python float or an IntoExprColumn (pl.Expr, pl.Series or column name str) carrying one value per row.

0.0
sigma float | IntoExprColumn

Scale of the underlying normal (std-dev of ln(X)), with sigma > 0. Same accepted types as mu.

1.0

An invalid parameterisation (sigma <= 0 or a non-finite parameter) is not checked at construction; it raises InvalidOperation (a ComputeError) when any method is evaluated.

Null parameters propagate to null.

Source code in polars_stats/distributions/_lognormal.py
def __init__(self, mu: float | IntoExprColumn = 0.0, sigma: float | IntoExprColumn = 1.0) -> None:
    self._mu = coerce_param(mu, name="mu")
    self._sigma = coerce_param(sigma, name="sigma")
    self._scalar_kwargs = scalar_kwargs(mu=scalar_float(mu), sigma=scalar_float(sigma))

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

mean

mean() -> Expr

Expected value, exp(mu + sigma ** 2 / 2).

Source code in polars_stats/distributions/_lognormal.py
def mean(self) -> pl.Expr:
    """Expected value, ``exp(mu + sigma ** 2 / 2)``."""
    return self._moment((self._mu + self._half_sigma_sq).exp())

variance

variance() -> Expr

Variance, (exp(sigma ** 2) - 1) * exp(2 * mu + sigma ** 2).

The leading factor is spelled 2 * exp(t / 2) * sinh(t / 2), which is expm1(t) identically. Polars has no expm1, and the literal exp(t) - 1 cancels for a small sigma. The identity holds full precision on both sides and overflows no earlier than the result does.

Source code in polars_stats/distributions/_lognormal.py
def variance(self) -> pl.Expr:
    """Variance, ``(exp(sigma ** 2) - 1) * exp(2 * mu + sigma ** 2)``.

    The leading factor is spelled ``2 * exp(t / 2) * sinh(t / 2)``, which is ``expm1(t)``
    identically. Polars has no ``expm1``, and the literal ``exp(t) - 1`` cancels for a small
    ``sigma``. The identity holds full precision on both sides and overflows no earlier than the
    result does.
    """
    half = self._half_sigma_sq
    return self._moment(2 * half.exp() * half.sinh() * (2 * self._mu + self._sigma**2).exp())

std

std() -> Expr

Standard deviation, exp(0.5 * log(exp(sigma ** 2) - 1) + mu + sigma ** 2 / 2).

Overrides the base-class variance().sqrt(), which inherits an overflow the square root would have undone: the variance genuinely exceeds f64 above sigma ~ 18.8 (so inf is right there), but the standard deviation only does above sigma ~ 26.6.

Consequence worth knowing: std() ** 2 and variance() are no longer interchangeable at a large sigma, because one is representable and the other is not.

Source code in polars_stats/distributions/_lognormal.py
def std(self) -> pl.Expr:
    """Standard deviation, ``exp(0.5 * log(exp(sigma ** 2) - 1) + mu + sigma ** 2 / 2)``.

    Overrides the base-class ``variance().sqrt()``, which inherits an overflow the square root
    would have undone: the variance genuinely exceeds ``f64`` above ``sigma ~ 18.8`` (so ``inf``
    is right *there*), but the standard deviation only does above ``sigma ~ 26.6``.

    Consequence worth knowing: ``std() ** 2`` and ``variance()`` are no longer interchangeable
    at a large ``sigma``, because one is representable and the other is not.
    """
    half = self._half_sigma_sq
    return self._moment((0.5 * (_LN_2 + half + half.sinh().log()) + self._mu + half).exp())

median

median() -> Expr

Median, exp(mu).

Source code in polars_stats/distributions/_lognormal.py
def median(self) -> pl.Expr:
    """Median, ``exp(mu)``."""
    return self._moment(self._mu.exp())

entropy

entropy() -> Expr

Differential entropy, mu + 0.5 * log(2 * pi * e * sigma ** 2).

Source code in polars_stats/distributions/_lognormal.py
def entropy(self) -> pl.Expr:
    """Differential entropy, ``mu + 0.5 * log(2 * pi * e * sigma ** 2)``."""
    return self._moment(self._mu + 0.5 * (_TWO_PI_E * self._sigma**2).log())

Normal

Normal(mu: float | IntoExprColumn = 0.0, sigma: float | IntoExprColumn = 1.0)

Bases: ContinuousDistribution

Normal (Gaussian) distribution with location mu and scale sigma.

Equivalent to scipy.stats.norm(loc=mu, scale=sigma). The standard normal (mu=0, sigma=1) is the default parameterisation.

Parameters:

Name Type Description Default
mu float | IntoExprColumn

Location parameter. Either a Python float or an IntoExprColumn (pl.Expr, pl.Series or column name str) carrying one location per row.

0.0
sigma float | IntoExprColumn

Scale parameter, with sigma > 0. Same accepted types as mu.

1.0

An invalid scale (sigma <= 0 or a non-finite parameter) is not checked at construction; it raises InvalidOperation (a ComputeError) when any method is evaluated.

Null parameters propagate to null.

Source code in polars_stats/distributions/_normal.py
def __init__(
    self,
    mu: float | IntoExprColumn = 0.0,
    sigma: float | IntoExprColumn = 1.0,
) -> None:
    self._mu = coerce_param(mu, name="mu")
    self._sigma = coerce_param(sigma, name="sigma")
    self._scalar_kwargs = scalar_kwargs(mu=scalar_float(mu), sigma=scalar_float(sigma))

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

mean

mean() -> Expr

Expected value, the mu location parameter.

Source code in polars_stats/distributions/_normal.py
def mean(self) -> pl.Expr:
    """Expected value, the ``mu`` location parameter."""
    return self._moment(self._mu)

variance

variance() -> Expr

Variance, sigma ** 2.

Source code in polars_stats/distributions/_normal.py
def variance(self) -> pl.Expr:
    """Variance, ``sigma ** 2``."""
    return self._moment(self._sigma**2)

std

std() -> Expr

Standard deviation, the sigma scale parameter.

Overrides the base-class variance().sqrt(), which squares sigma and then unsquares it: the round trip saturates across roughly 300 decades where sigma is the answer.

Source code in polars_stats/distributions/_normal.py
def std(self) -> pl.Expr:
    """Standard deviation, the ``sigma`` scale parameter.

    Overrides the base-class ``variance().sqrt()``, which squares ``sigma`` and then unsquares
    it: the round trip saturates across roughly 300 decades where ``sigma`` is the answer.
    """
    return self._moment(self._sigma)

median

median() -> Expr

Median, equal to the mu location parameter.

Source code in polars_stats/distributions/_normal.py
def median(self) -> pl.Expr:
    """Median, equal to the ``mu`` location parameter."""
    return self._moment(self._mu)

entropy

entropy() -> Expr

Differential entropy, 0.5 * log(2 * pi * e * sigma ** 2).

Source code in polars_stats/distributions/_normal.py
def entropy(self) -> pl.Expr:
    """Differential entropy, ``0.5 * log(2 * pi * e * sigma ** 2)``."""
    return self._moment(0.5 * (_TWO_PI_E * self._sigma**2).log())

Uniform

Uniform(min: float | IntoExprColumn, max: float | IntoExprColumn)

Bases: ContinuousDistribution

Continuous uniform distribution over [min, max].

Equivalent to scipy.stats.uniform(loc=min, scale=max - min).

Following scipy, the density, cdf and the other closed forms treat the support as the closed interval [min, max] (so pdf(max) == 1 / (max - min)); the sample plugin draws on the half-open [min, max).

Parameters:

Name Type Description Default
min float | IntoExprColumn

Lower bound. Either a Python float or an IntoExprColumn (pl.Expr, pl.Series or column name str) carrying one bound per row.

required
max float | IntoExprColumn

Upper bound, with max > min. Same accepted types as min.

required

An invalid parameterisation (max <= min, a non-finite bound, or a width max - min overflowing float64) is not checked at construction; matching every other distribution, it raises InvalidOperation (a ComputeError) when any method is evaluated. Null bounds propagate to null.

Source code in polars_stats/distributions/_uniform.py
def __init__(self, min: float | IntoExprColumn, max: float | IntoExprColumn) -> None:  # noqa: A002
    self._min = coerce_param(min, name="min")
    self._max = coerce_param(max, name="max")
    self._scalar_kwargs = scalar_kwargs(min=scalar_float(min), max=scalar_float(max))

range property

range: Expr

Width of the support, max - min.

Validated in Rust so an invalid parameterisation (max <= min, a non-finite bound, or a width overflowing float64) raises rather than silently yielding a non-positive or infinite width. Every closed-form method (moments and pdf/cdf/ppf) derives from this, so they all validate consistently; null bounds propagate. See _checked for the scalar-vs-column routing.

sample

sample(seed: int | None = None) -> Expr

Draw one random variate per row.

Returns a column with one variate per input row, in the distribution's element dtype (Float64, UInt64 or Boolean). Output length follows the surrounding context (frame length under select / with_columns, partition length under over / group_by), and each row's draw is derived from a per-row sub-seed mixed from seed and the row's position, so the result is independent of Polars chunking and thread scheduling.

A row with an invalid parameter raises; a row with a null parameter yields null. The output is named "sample" when every parameter is constant (the fast path); with any column-valued parameter the name follows the first parameter expression (polars root-name semantics, so .name.* modifiers keep working).

Source code in polars_stats/distributions/_base.py
def sample(self, seed: int | None = None) -> pl.Expr:
    """Draw one random variate per row.

    Returns a column with one variate per input row, in the distribution's element dtype
    (`Float64`, `UInt64` or `Boolean`). Output length follows the surrounding context (frame length
    under `select` / `with_columns`, partition length under `over` / `group_by`), and each row's draw
    is derived from a per-row sub-seed mixed from `seed` and the row's position, so the result is
    independent of Polars chunking and thread scheduling.

    A row with an invalid parameter raises; a row with a null parameter yields null. The output is
    named `"sample"` when every parameter is constant (the fast path); with any column-valued
    parameter the name follows the first parameter expression (polars root-name semantics, so
    `.name.*` modifiers keep working).
    """
    if self._scalar_kwargs is not None:
        return register_plugin(
            f"{self._plugin_prefix}_sample_scalar",
            (ROW_INDEX_EXPR,),
            kwargs={"seed": seed, **self._scalar_kwargs},
        ).alias("sample")
    return register_plugin(
        f"{self._plugin_prefix}_sample", (*self._param_exprs, ROW_INDEX_EXPR), kwargs={"seed": seed}
    )

samples

samples(size: int, seed: int | None = None) -> Expr

Draw size random variates per row, returning Array(inner=<element dtype>, shape=size).

Each row's size draws are consecutive values from one per-row random stream keyed by seed and the row's position, so the result is reproducible for a fixed seed and independent of Polars chunking and thread scheduling. samples(size=1) matches sample for the same seed, and growing size extends each row's array without changing the existing draws.

A row with a null parameter yields a null array (not an array of null elements), produced natively by the plugin via the output's outer validity; an invalid parameterisation raises.

Naming follows sample: "samples" with all-constant parameters, the first parameter expression's root name otherwise.

Source code in polars_stats/distributions/_base.py
def samples(self, size: int, seed: int | None = None) -> pl.Expr:
    """Draw `size` random variates per row, returning `Array(inner=<element dtype>, shape=size)`.

    Each row's `size` draws are consecutive values from one per-row random stream keyed by `seed` and the
    row's position, so the result is reproducible for a fixed `seed` and independent of Polars chunking and
    thread scheduling. `samples(size=1)` matches `sample` for the same seed, and growing `size` extends each
    row's array without changing the existing draws.

    A row with a null parameter yields a null array (not an array of null elements), produced natively by
    the plugin via the output's outer validity; an invalid parameterisation raises.

    Naming follows `sample`: `"samples"` with all-constant parameters, the first parameter
    expression's root name otherwise.
    """
    if size <= 0:
        msg = f"size must be a positive integer, got {size}"
        raise ValueError(msg)
    out = self._samples(size=size, seed=seed)
    return out.alias("samples") if self._scalar_kwargs is not None else out

cdf

cdf(value: float | IntoExprColumn) -> Expr

Cumulative distribution function, P(X <= value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Cumulative distribution function, `P(X <= value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._cdf(v))

log_cdf

log_cdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the cdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_cdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the cdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_cdf(v))

sf

sf(value: float | IntoExprColumn) -> Expr

Survival function, P(X > value) = 1 - cdf(value). Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Survival function, `P(X > value) = 1 - cdf(value)`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._sf(v))

log_sf

log_sf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the survival function. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_sf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the survival function. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_sf(v))

ppf

ppf(quantile: float | IntoExprColumn) -> Expr

Percent point function (inverse cdf).

A quantile outside [0, 1] yields null.

Nulls are propagated and a NaN quantile yields NaN, matching scipy.

Source code in polars_stats/distributions/_base.py
def ppf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Percent point function (inverse cdf).

    A `quantile` outside `[0, 1]` yields **null**.

    Nulls are propagated and a `NaN` quantile yields `NaN`, matching scipy.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._ppf(q))

isf

isf(quantile: float | IntoExprColumn) -> Expr

Inverse survival function, the value x with sf(x) == quantile.

Same domain contract as ppf, with the endpoints reversed: quantile outside [0, 1] yields null, nulls propagate, NaN yields NaN.

Source code in polars_stats/distributions/_base.py
def isf(self, quantile: float | IntoExprColumn) -> pl.Expr:
    """Inverse survival function, the value `x` with `sf(x) == quantile`.

    Same domain contract as `ppf`, with the endpoints reversed: `quantile` outside `[0, 1]` yields
    null, nulls propagate, `NaN` yields `NaN`.
    """
    q = as_expr(quantile)
    return propagate_null_and_nan(q, self._isf(q))

pdf

pdf(value: float | IntoExprColumn) -> Expr

Probability density function evaluated at value. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Probability density function evaluated at `value`. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._pdf(v))

log_pdf

log_pdf(value: float | IntoExprColumn) -> Expr

Natural logarithm of the pdf. Nulls and NaNs in value are propagated.

Source code in polars_stats/distributions/_base.py
def log_pdf(self, value: float | IntoExprColumn) -> pl.Expr:
    """Natural logarithm of the pdf. Nulls and NaNs in `value` are propagated."""
    v = as_expr(value)
    return propagate_null_and_nan(v, self._log_pdf(v))

mean

mean() -> Expr

Expected value, (min + max) / 2.

Source code in polars_stats/distributions/_uniform.py
def mean(self) -> pl.Expr:
    """Expected value, ``(min + max) / 2``."""
    return self._min + self.range / 2

variance

variance() -> Expr

Variance, (max - min)^2 / 12.

Source code in polars_stats/distributions/_uniform.py
def variance(self) -> pl.Expr:
    """Variance, ``(max - min)^2 / 12``."""
    return self.range**2 / 12

std

std() -> Expr

Standard deviation, (max - min) / sqrt(12).

Overrides the base-class variance().sqrt(), which squares the span and then unsquares it: the round trip saturates about 300 decades before the answer does. Dividing by sqrt(12) once also drops a rounding.

Source code in polars_stats/distributions/_uniform.py
def std(self) -> pl.Expr:
    """Standard deviation, ``(max - min) / sqrt(12)``.

    Overrides the base-class ``variance().sqrt()``, which squares the span and then unsquares
    it: the round trip saturates about 300 decades before the answer does. Dividing by
    ``sqrt(12)`` once also drops a rounding.
    """
    return self.range / math.sqrt(12)

median

median() -> Expr

Median, (min + max) / 2.

Source code in polars_stats/distributions/_uniform.py
def median(self) -> pl.Expr:
    """Median, ``(min + max) / 2``."""
    return self._min + self.range / 2

entropy

entropy() -> Expr

Differential entropy, log(max - min).

Source code in polars_stats/distributions/_uniform.py
def entropy(self) -> pl.Expr:
    """Differential entropy, ``log(max - min)``."""
    return self.range.log()