Source code for mojolearn.resample

# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Andrew Hendel. Part of mojolearn, https://doi.org/10.5281/zenodo.22068632
# Maintainer notes (moved from the module docstring): workstream D, 2026-09-14. This module is the
# Python door of `resample/estimator.mojo` through `bindings/_mojolearn_resample.mojo`;
# `resample/README.md` carries the SciPy parameter mapping. The point estimate, interval, standard
# error and p-value are host scalars over the same pinned tree
# (`metrics/checks/pinned_sum.mojo::host_tree_sum`), with no libm call. BCa: DEVIATION 1699, closed by
# DEVIATION 5410. Mojo-host refusals: `n_resamples` outside the positions the index map can address
# (`validate_positions`), a pooled size the permutation map cannot address, and the
# `n_resamples * n` sort-cell ceiling. No published speed number for this lane.
"""Resampling statistics: bootstrap intervals, permutation tests, Monte Carlo
integration and scikit-learn style row resampling.

The public functions are :func:`bootstrap`, :func:`permutation_test`,
:func:`monte_carlo_integrate`, :func:`resample` and :func:`resample_indices`.
Their parameters mean what SciPy's (or scikit-learn's) parameters of the same
name mean. The random draws and the per-replicate statistics run on the
device, one block per replicate; the point estimate, interval, standard error
and p-value are then computed from those replicates in compiled code.

Inputs are converted to float32. Unknown statistic, method, alternative or
integrand names raise ``ValueError``, as does a sample that is not 1-D or
2-D, a non-finite value, a ``confidence_level`` outside (0, 1), a statistic
that reads a column the sample does not have, ``'std'`` on fewer than two
rows, or an ``n_resamples`` too large to address.

``r_first`` (and ``i_first`` for Monte Carlo) selects where a run starts in
the replicate stream: replicates ``[r_first, r_first + n_resamples)`` are
bit-identical to the same slice of one whole run, so a long run can be split
into batches.

Every function takes ``numeric_mode`` (``'identical'``, ``'deterministic'``,
or ``'fast'`` on Apple); ``None`` uses the package default.

Examples
--------
>>> import numpy as np, mojolearn
>>> x = np.random.default_rng(0).random(1000, dtype=np.float32)
>>> res = mojolearn.resample.bootstrap(x, statistic="mean", n_resamples=999)
>>> low, high = res.confidence_interval
"""
from collections import namedtuple

from . import _backend
from ._array import Array
from ._buffer import addr, addr_ro, as_f32_c, empty

_MODE_CODE = {"fast": 0, "identical": 1, "deterministic": 2}

# Codes are `resample/checks/statistics.mojo`'s STAT_* codes.
#: Statistic names accepted by :func:`bootstrap` and :func:`permutation_test`, with their integer codes.
STATISTICS = {"mean": 0, "std": 1, "quantile": 2, "pearson": 3, "diff_means": 4, "trimmed_mean": 5}
# Codes are `resample/checks/intervals.mojo`'s METHOD_* codes.
#: Confidence-interval methods accepted by :func:`bootstrap` (``method`` is case-insensitive).
METHODS = {"percentile": 0, "basic": 1, "bca": 2}
# Codes are `resample/checks/intervals.mojo`'s ALT_* codes.
#: Alternative hypotheses accepted by :func:`bootstrap` and :func:`permutation_test`.
ALTERNATIVES = {"two-sided": 0, "less": 1, "greater": 2}
# Codes are `resample/checks/statistics.mojo`'s MC_F_* codes.
#: Integrands accepted by :func:`monte_carlo_integrate`.
INTEGRANDS = {"const": 0, "sum": 1, "product": 2}


# Sep 22 pip smoke: `res.confidence_interval.low`, SciPy's spelling, raised AttributeError on the
# plain tuple, hence the namedtuple.
#: SciPy's ``ConfidenceInterval``: a 2-tuple ``(low, high)`` that also has ``.low`` and ``.high``.
ConfidenceInterval = namedtuple("ConfidenceInterval", ["low", "high"])


[docs] class BootstrapResult: """Result of :func:`bootstrap`, SciPy's ``BootstrapResult`` with extra fields. Attributes ---------- point_estimate : float The statistic on the original sample. distribution : Array of float32, shape (n_resamples,) The bootstrap replicates, in replicate order. sorted_distribution : Array of float32, shape (n_resamples,) The same replicates sorted ascending. standard_error : float Standard deviation of the bootstrap distribution. confidence_interval : ConfidenceInterval ``(low, high)``; also available as ``.low`` and ``.high``. order_low, order_high : int Positions in ``sorted_distribution`` used for the interval ends (for ``method='BCa'``, the positions at its adjusted levels). """ def __init__(self, point_estimate, distribution, sorted_distribution, standard_error, low, high, order_low, order_high): self.point_estimate = point_estimate self.distribution = distribution self.sorted_distribution = sorted_distribution self.standard_error = standard_error self.confidence_interval = ConfidenceInterval(low, high) self.order_low = order_low self.order_high = order_high
[docs] class PermutationTestResult: """Result of :func:`permutation_test`, SciPy's ``PermutationTestResult`` with extra fields. Attributes ---------- statistic : float The statistic on the observed samples. null_distribution : Array of float32, shape (n_resamples,) The statistic on each permutation. pvalue : float The p-value for the requested ``alternative``. count_less, count_greater : int The two counts the p-value was computed from: null values on the low side and on the high side of the observed statistic. """ def __init__(self, statistic, null_distribution, pvalue, count_less, count_greater): self.statistic = statistic self.null_distribution = null_distribution self.pvalue = pvalue self.count_less = count_less self.count_greater = count_greater
[docs] class MonteCarloResult: """Result of :func:`monte_carlo_integrate`. Attributes ---------- integral : float The estimate, ``volume * mean``. mean : float Mean of the integrand over the draws. volume : float Area of the integration box. closed_form : float The exact integral of the same integrand over the same box, so the estimate's error can be read without a second call. """ def __init__(self, integral, mean, volume, closed_form): self.integral = integral self.mean = mean self.volume = volume self.closed_form = closed_form
def _extension(numeric_mode=None): mod = _backend.binding("_mojolearn_resample", numeric_mode) want = numeric_mode or _backend.default_mode() fn = getattr(mod, "resample_numeric_mode", None) if fn is not None: got = int(fn()) if got != _MODE_CODE.get(want): raise RuntimeError( f"mojolearn resample: numeric_mode={want!r} was requested but " f"{mod.__name__} reports compile-time mode code {got}; rebuild it " "with bash bindings/build_resample.sh" ) return mod def _code(table, value, name, where): if isinstance(value, str): if value not in table: raise ValueError( f"mojolearn {where}: {name} must be one of {sorted(table)}, got {value!r}" ) return table[value] if isinstance(value, bool) or not isinstance(value, int): raise TypeError(f"mojolearn {where}: {name} must be a name or its code") return int(value) def _real(v, name, where): if isinstance(v, bool) or not isinstance(v, (int, float)): raise TypeError(f"mojolearn {where}: {name} must be a real number, got {type(v).__name__}") return float(v) def _int(v, name, where): if isinstance(v, bool) or not isinstance(v, int): raise TypeError(f"mojolearn {where}: {name} must be an int, got {type(v).__name__}") return int(v) # BCa ships for mean, std and diff_means (DEVIATION 1699, closed by DEVIATION 5410); refused by name on the # Mojo host for other statistics and a degenerate interval. Unpaired: sample 0 by the one-sample map, # sample 1 by its own.
[docs] def bootstrap(data, statistic="mean", n_resamples=9999, confidence_level=0.95, method="percentile", alternative="two-sided", random_state=0, q_or_prop=0.5, r_first=0, numeric_mode=None, paired=True): """Bootstrap confidence interval for a statistic. Follows ``scipy.stats.bootstrap((data,), statistic, n_resamples=..., rng=random_state, method=..., confidence_level=..., alternative=...)``. The statistic is chosen by name from a compiled set rather than passed as a callable. The resampling and the per-replicate statistic run on the device. Parameters ---------- data : array-like of shape (n,) or (n, 2), or a pair (x, y) The sample, converted to float32. A two-column sample keeps its row pairing (SciPy's ``paired=True``), which ``'pearson'`` and ``'diff_means'`` read. With ``paired=False``, a pair of 1-D samples of any lengths. statistic : str or int, default='mean' ``'mean'``, ``'std'``, ``'quantile'``, ``'pearson'``, ``'diff_means'`` or ``'trimmed_mean'``, or its code in :data:`STATISTICS`. n_resamples : int, default=9999 Number of bootstrap replicates. confidence_level : float, default=0.95 Confidence level of the interval, in (0, 1). method : {'percentile', 'basic', 'BCa'}, default='percentile' Interval method, case-insensitive as in SciPy. ``'BCa'`` is available for ``'mean'``, ``'std'`` and ``'diff_means'`` and raises ``ValueError`` for the other statistics or a degenerate interval. alternative : {'two-sided', 'less', 'greater'}, default='two-sided' Which side(s) of the interval to bound. random_state : int, default=0 Seed of the replicate stream. q_or_prop : float, default=0.5 ``q`` for ``'quantile'`` and ``proportiontocut`` for ``'trimmed_mean'``; ignored otherwise. r_first : int, default=0 Index of the first replicate. Replicates ``[r_first, r_first + n_resamples)`` equal the same slice of one whole run, bit for bit. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default=None Numeric mode (``'fast'`` is Apple only); ``None`` uses the package default. paired : bool, default=True ``False`` with ``data=(x, y)`` and ``statistic='diff_means'`` runs SciPy's unpaired two-sample bootstrap, each sample resampled independently; any other statistic raises ``ValueError``. A single sample ignores ``paired``, as SciPy does. Returns ------- BootstrapResult Raises ------ ValueError For an unknown name, a sample that is not 1-D or 2-D, a non-finite value, a ``confidence_level`` outside (0, 1), ``'std'`` on fewer than two rows, or an unsupported ``method`` / ``statistic`` combination. TypeError For a parameter of the wrong type. Examples -------- >>> import numpy as np, mojolearn >>> x = np.random.default_rng(0).random(1000, dtype=np.float32) >>> res = mojolearn.resample.bootstrap(x, statistic="mean", method="BCa") >>> low, high = res.confidence_interval """ where = "bootstrap" if not paired and isinstance(data, (tuple, list)): return _bootstrap_unpaired(data, statistic, n_resamples, confidence_level, method, alternative, random_state, r_first, numeric_mode) x, _ = as_f32_c(data, ndim=None, name="data") if x.ndim == 1: n, d = x.shape[0], 1 elif x.ndim == 2: n, d = x.shape else: raise ValueError(f"mojolearn {where}: data must be 1-D or 2-D, got {x.ndim}-D") stat = _code(STATISTICS, statistic, "statistic", where) meth = _code(METHODS, method.lower() if isinstance(method, str) else method, "method", where) alt = _code(ALTERNATIVES, alternative, "alternative", where) r = _int(n_resamples, "n_resamples", where) rf = _int(r_first, "r_first", where) seed = _int(random_state, "random_state", where) cl = _real(confidence_level, "confidence_level", where) qp = _real(q_or_prop, "q_or_prop", where) flat = x.reshape((n * d,)) dist = empty((max(r, 0),), "<f4") sdist = empty((max(r, 0),), "<f4") scalars = empty((6,), "<f8") _extension(numeric_mode).bootstrap( # ORDER MATCHES bindings/_mojolearn_resample.mojo::bootstrap_binding. # x, distribution_out, sorted_out, scalars_out [addr_ro(flat, name="data"), addr(dist, name="distribution"), addr(sdist, name="sorted_distribution"), addr(scalars, name="scalars")], # n, d, statistic, n_resamples, seed, method, confidence_level, alternative, q_or_prop, r_first [n, d, stat, r, seed, meth, cl, alt, qp, rf], ) return BootstrapResult(float(scalars[0]), dist, sdist, float(scalars[1]), float(scalars[2]), float(scalars[3]), int(scalars[4]), int(scalars[5]))
def _bootstrap_unpaired(data, statistic, n_resamples, confidence_level, method, alternative, random_state, r_first, numeric_mode): where = "bootstrap(paired=False)" if len(data) != 2: raise ValueError(f"mojolearn {where}: data must be two samples (x, y), got {len(data)}") if statistic != "diff_means": raise ValueError( f"mojolearn {where}: statistic {statistic!r} is refused by name: the unpaired bootstrap's" " two-sample statistic is 'diff_means' (a one-sample statistic has no pairing; pearson needs pairs)") xx, _ = as_f32_c(data[0], ndim=1, name="x") yy, _ = as_f32_c(data[1], ndim=1, name="y") meth = _code(METHODS, method.lower() if isinstance(method, str) else method, "method", where) alt = _code(ALTERNATIVES, alternative, "alternative", where) r = _int(n_resamples, "n_resamples", where) dist = empty((max(r, 0),), "<f4") sdist = empty((max(r, 0),), "<f4") scalars = empty((6,), "<f8") _extension(numeric_mode).bootstrap_unpaired( # ORDER MATCHES bindings/_mojolearn_resample.mojo::bootstrap_unpaired_binding. [addr_ro(xx, name="x"), addr_ro(yy, name="y"), addr(dist, name="distribution"), addr(sdist, name="sorted_distribution"), addr(scalars, name="scalars")], # n_x, n_y, n_resamples, seed, method, confidence_level, alternative, r_first [xx.shape[0], yy.shape[0], r, _int(random_state, "random_state", where), meth, _real(confidence_level, "confidence_level", where), alt, _int(r_first, "r_first", where)], ) return BootstrapResult(float(scalars[0]), dist, sdist, float(scalars[1]), float(scalars[2]), float(scalars[3]), int(scalars[4]), int(scalars[5])) # The null is never exhaustive (DEVIATION 1702). 'pairings' is refused: for the implemented statistics # its null leaves the statistic unchanged, and pearson has no permutation arm (resample/NOT_IMPLEMENTED.tsv).
[docs] def permutation_test(x, y=None, statistic="diff_means", n_resamples=9999, alternative="two-sided", random_state=0, r_first=0, numeric_mode=None, permutation_type="independent"): """Permutation test of a statistic. Follows ``scipy.stats.permutation_test((x, y), statistic, permutation_type=..., n_resamples=..., rng=random_state, alternative=...)``, with the statistic chosen by name. Unlike SciPy, the null distribution is always sampled with ``n_resamples`` permutations, never enumerated exhaustively; the p-value is conservative. Parameters ---------- x : array-like of shape (n_x,) First sample, converted to float32. y : array-like of shape (n_y,) or None, default=None Second sample. Required for ``permutation_type='independent'``. With ``'samples'``, ``y`` is the paired second sample, or ``None`` for a one-sample sign-flip test. statistic : str or int, default='diff_means' The statistic, by name or by its code in :data:`STATISTICS`. With ``permutation_type='samples'`` it must be ``'diff_means'`` for two paired samples and ``'mean'`` for one sample. n_resamples : int, default=9999 Number of permutations. alternative : {'two-sided', 'less', 'greater'}, default='two-sided' The alternative hypothesis. random_state : int, default=0 Seed of the permutation stream. r_first : int, default=0 Index of the first permutation; a slice of permutations equals the same slice of one whole run, bit for bit. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default=None Numeric mode (``'fast'`` is Apple only); ``None`` uses the package default. permutation_type : {'independent', 'samples'}, default='independent' ``'independent'`` reassigns pooled observations between ``x`` and ``y``. ``'samples'`` swaps the two observations of each pair at random (two paired samples), or flips the sign of each observation (one sample), as in SciPy. ``'pairings'`` raises ``ValueError``. Returns ------- PermutationTestResult Raises ------ ValueError For an unknown name, a missing ``y``, a refused ``permutation_type`` or ``statistic`` combination, or a non-finite value. Examples -------- >>> import numpy as np, mojolearn >>> rng = np.random.default_rng(0) >>> x = rng.random(200, dtype=np.float32) >>> y = rng.random(300, dtype=np.float32) + 0.1 >>> res = mojolearn.resample.permutation_test(x, y, n_resamples=999) >>> res.pvalue # doctest: +SKIP """ where = "permutation_test" ptype = permutation_type.lower() if isinstance(permutation_type, str) else permutation_type if ptype == "samples": return _permutation_samples(x, y, statistic, n_resamples, alternative, random_state, r_first, numeric_mode) if ptype == "pairings": raise ValueError( f"mojolearn {where}: permutation_type='pairings' is refused by name: its null permutes every" " sample's observation order, which for the implemented statistics (mean, std, diff_means)" " leaves the statistic unchanged -- the null is the observed value R times; pearson, the" " statistic it exists for, has no permutation arm (resample/NOT_IMPLEMENTED.tsv)") if ptype != "independent": raise ValueError(f"mojolearn {where}: permutation_type must be 'independent', 'samples' or" f" 'pairings', got {permutation_type!r}") if y is None: raise ValueError(f"mojolearn {where}: permutation_type='independent' needs two samples x and y") xx, _ = as_f32_c(x, ndim=1, name="x") yy, _ = as_f32_c(y, ndim=1, name="y") stat = _code(STATISTICS, statistic, "statistic", where) alt = _code(ALTERNATIVES, alternative, "alternative", where) r = _int(n_resamples, "n_resamples", where) rf = _int(r_first, "r_first", where) seed = _int(random_state, "random_state", where) null = empty((max(r, 0),), "<f4") scalars = empty((4,), "<f8") _extension(numeric_mode).permutation_test( # ORDER MATCHES bindings/_mojolearn_resample.mojo::permutation_test_binding. # x, y, null_out, scalars_out [addr_ro(xx, name="x"), addr_ro(yy, name="y"), addr(null, name="null_distribution"), addr(scalars, name="scalars")], # n_x, n_y, statistic, n_resamples, seed, alternative, r_first [xx.shape[0], yy.shape[0], stat, r, seed, alt, rf], ) return PermutationTestResult(float(scalars[0]), null, float(scalars[1]), int(scalars[2]), int(scalars[3]))
def _permutation_samples(x, y, statistic, n_resamples, alternative, random_state, r_first, numeric_mode): """permutation_type='samples': (x, y) paired, diff_means, each pair's two observations traded by a fair coin; (x,) alone (y None), mean, the sign of each observation flipped (SciPy's one-sample convention).""" where = "permutation_test(permutation_type='samples')" xx, _ = as_f32_c(x, ndim=1, name="x") want = "mean" if y is None else "diff_means" if statistic != want: raise ValueError( f"mojolearn {where}: statistic {statistic!r} is refused by name; the implemented arm for " f"{'one sample' if y is None else 'two paired samples'} is {want!r}") if y is None: yy, ny = xx, 0 else: yy, _ = as_f32_c(y, ndim=1, name="y") ny = yy.shape[0] r = _int(n_resamples, "n_resamples", where) null = empty((max(r, 0),), "<f4") scalars = empty((4,), "<f8") _extension(numeric_mode).permutation_samples( # ORDER MATCHES bindings/_mojolearn_resample.mojo::permutation_samples_binding. [addr_ro(xx, name="x"), addr_ro(yy, name="y"), addr(null, name="null_distribution"), addr(scalars, name="scalars")], # n, n_y, n_resamples, seed, alternative, r_first [xx.shape[0], ny, r, _int(random_state, "random_state", where), _code(ALTERNATIVES, alternative, "alternative", where), _int(r_first, "r_first", where)], ) return PermutationTestResult(float(scalars[0]), null, float(scalars[1]), int(scalars[2]), int(scalars[3])) # Three compiled integrand arms; the closed form is hand-derived. The box has MC_DIMS = 2 dimensions.
[docs] def monte_carlo_integrate(integrand, lower, upper, n_samples, random_state=0, i_first=0, numeric_mode=None): """Monte Carlo estimate of a 2-D integral over a box. Computes ``volume * mean(f(x_i))`` over ``n_samples`` uniform draws from the two-dimensional box ``[lower, upper)``. The exact value of the same integral is returned beside the estimate. Parameters ---------- integrand : {'const', 'sum', 'product'} or int ``'const'`` (f = 1), ``'sum'`` (x0 + x1) or ``'product'`` (x0 * x1), or its code in :data:`INTEGRANDS`. lower, upper : sequence of 2 floats Lower and upper corners of the box; ``ValueError`` if either does not hold exactly two values. n_samples : int Number of uniform draws. random_state : int, default=0 Seed of the draw stream. i_first : int, default=0 Index of the first draw, so a long run can be split into batches. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default=None Numeric mode (``'fast'`` is Apple only); ``None`` uses the package default. Returns ------- MonteCarloResult Examples -------- >>> import mojolearn >>> res = mojolearn.resample.monte_carlo_integrate("product", [0, 0], [1, 2], 100000) >>> round(res.closed_form, 6) 1.0 """ where = "monte_carlo_integrate" f_id = _code(INTEGRANDS, integrand, "integrand", where) lo = Array.from_list([_real(v, "lower", where) for v in lower], "<f4") # glue: validates the two lower bounds hi = Array.from_list([_real(v, "upper", where) for v in upper], "<f4") # glue: validates the two upper bounds if lo.shape != (2,) or hi.shape != (2,): raise ValueError( f"mojolearn {where}: lower and upper must each hold 2 values " f"(MC_DIMS), got {lo.shape[0]} and {hi.shape[0]}" ) scalars = empty((4,), "<f8") _extension(numeric_mode).monte_carlo_integrate( # ORDER MATCHES bindings/_mojolearn_resample.mojo::monte_carlo_integrate_binding. # lower, upper, scalars_out [addr_ro(lo, name="lower"), addr_ro(hi, name="upper"), addr(scalars, name="scalars")], # f_id, n_samples, seed, i_first [f_id, _int(n_samples, "n_samples", where), _int(random_state, "random_state", where), _int(i_first, "i_first", where)], ) return MonteCarloResult(float(scalars[0]), float(scalars[1]), float(scalars[2]), float(scalars[3]))
# replace=True: position i draws a row by the Philox position map (kind 6); replace=False keeps the first # n_samples positions of a keyed total order (kind 7, `rng.permutation(n)[:n_samples]`'s positional # spelling). Integer only, so the same indices on every vendor and on the CPU.
[docs] def resample_indices(n, n_samples=None, replace=True, random_state=0, numeric_mode=None): """Row indices for a scikit-learn style resample. Returns the indices :func:`resample` gathers. With ``replace=True`` each position draws a row independently; with ``replace=False`` the result is the first ``n_samples`` entries of a random permutation of ``range(n)``. The draw is integer only, so the same seed gives the same indices on every device. The indices differ from scikit-learn's for the same ``random_state``. Parameters ---------- n : int Number of rows to draw from. n_samples : int or None, default=None Number of indices; ``None`` means ``n``. With ``replace=False`` it must be at most ``n``. replace : bool, default=True Draw with replacement. random_state : int, default=0 Seed. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default=None Numeric mode (``'fast'`` is Apple only); ``None`` uses the package default. Returns ------- Array of int32, shape (n_samples,) Examples -------- >>> import mojolearn >>> idx = mojolearn.resample.resample_indices(1000, n_samples=100, replace=False) """ where = "resample" n = _int(n, "n", where) count = n if n_samples is None else _int(n_samples, "n_samples", where) idx = empty((max(count, 0),), "<i4") _extension(numeric_mode).resample_indices( # ORDER MATCHES bindings/_mojolearn_resample.mojo::resample_indices_binding. [addr(idx, name="indices")], [n, count, 1 if replace else 0, _int(random_state, "random_state", where)], ) return idx
def _take(a, idx): if hasattr(a, "__array__") and hasattr(a, "shape"): from ._optional_numpy import require_numpy np = require_numpy('resample') return np.asarray(a)[np.asarray(idx, dtype=np.intp)] return [a[int(i)] for i in idx] # cpu-route: Python list input taken by index [py-data-loop] def _gpu_gather(arrays, n, count, seed, numeric_mode, replace=True): """Return owned results or None for unchanged public fallback.""" # C11 IDENTICAL admission is the compiled binding's explicit opt-in; # metadata chooses a binding, all draws and gathered data stay in Mojo. if count <= 0 or n <= 0: return None mod = _extension(numeric_mode) enabled = getattr(mod, "resample_gpu_gather_enabled", None) if enabled is None or not int(enabled()): return None from ._optional_numpy import require_numpy try: np = require_numpy("resample") except ImportError: return None # Never coerce input: preserve existing list/Array/strided/dtype behavior. if any(not isinstance(x, np.ndarray) or x.dtype != np.float32 or x.ndim not in (1, 2) or not x.flags.c_contiguous or (x.ndim == 2 and not 0 < x.shape[1] <= 2147483647) for x in arrays): # glue: inspect dtype rank and strides of each argument return None outputs = [np.empty((count,) + x.shape[1:], dtype=np.float32) for x in arrays] # glue: allocate one caller-owned output per argument addresses = [] widths = [] for x, output in zip(arrays, outputs): # glue: one native span per argument addresses.extend((addr_ro(x, name="array"), addr(output, name="resampled"))) widths.append(1 if x.ndim == 1 else x.shape[1]) route = mod.resample_gather_gpu if replace else getattr(mod, "resample_permutation_gather_gpu", None) if route is not None and int(route(addresses, [n, count, seed] + widths)): return outputs return None def _narrow_gather(arrays, n, count, seed, numeric_mode): """`-D MOJOLEARN_RESAMPLE_FAST_GATHER_NARROW` (resample/estimator.mojo `resample_gather_narrow`): the indices drawn once on the device, the float32 C-contiguous arrays whose row is at most the binding's byte bound gathered there, the rest by `_take` with the same indices. None when the build lacks the define (main's route then runs).""" if count <= 0 or n <= 0 or not fast_defines(numeric_mode) & FAST_DEFINE_GATHER_NARROW: return None mod = _extension(numeric_mode) fn = getattr(mod, "resample_gather_narrow", None) if fn is None: return None from ._optional_numpy import require_numpy try: np = require_numpy("resample") except ImportError: return None bound = int(mod.resample_gather_narrow_bytes()) idx = empty((count,), "<i4") outs, addresses, widths = [], [addr(idx, name="indices")], [] for x in arrays: # glue: one native span per argument (dtype, rank and stride checks) w = (1 if x.ndim == 1 else x.shape[1]) if isinstance(x, np.ndarray) and x.ndim in (1, 2) else 0 if not (w > 0 and 4 * w <= bound and x.dtype == np.float32 and x.flags.c_contiguous): w = 0 out = np.empty((count,) + x.shape[1:], dtype=np.float32) if w else None outs.append(out) addresses.extend((addr_ro(x, name="array"), addr(out, name="resampled")) if w else (0, 0)) widths.append(w) done = int(fn(addresses, [n, count, seed] + widths)) if done < 0: return None return [outs[a] if (done >> a) & 1 else _take(x, idx) for a, x in enumerate(arrays)] # glue: one result per argument #: Bits of the resample binding's `resample_fast_defines()` mask #: (resample/estimator.mojo): FAST + Apple candidate defines, default OFF. FAST_DEFINE_CV_SLICE = 32 FAST_DEFINE_CV_TRUST_FOLDS = 64 FAST_DEFINE_GATHER_NARROW = 128 def fast_defines(numeric_mode=None): """The FAST + Apple candidate defines the resample binding was built with (`-D MOJOLEARN_RESAMPLE_FAST_*`, `-D MOJOLEARN_CV_FAST_*`), as the binding's bit mask; 0 off the FAST tier, on a build without the function, or when the binding cannot be loaded. No environment read.""" if (numeric_mode or _backend.default_mode()) != "fast": return 0 try: mod = _extension(numeric_mode) except Exception: return 0 fn = getattr(mod, "resample_fast_defines", None) return 0 if fn is None else int(fn()) # stratify and sample_weight are refused by name (resample/NOT_IMPLEMENTED.tsv).
[docs] def resample(*arrays, replace=True, n_samples=None, random_state=0, stratify=None, sample_weight=None, numeric_mode=None): """Resample arrays consistently along the first axis. Follows ``sklearn.utils.resample(*arrays, replace=..., n_samples=..., random_state=...)``: every array is indexed by the same rows, from :func:`resample_indices`. One array returns the resampled array, several return a list, as in scikit-learn. The rows drawn differ from scikit-learn's for the same ``random_state``. Parameters ---------- *arrays : sequence of array-like Arrays with the same first dimension. NumPy arrays are indexed as arrays; other sequences return lists. replace : bool, default=True Draw with replacement. n_samples : int or None, default=None Number of rows to draw; ``None`` means the length of the arrays. With ``replace=False`` it must be at most that length. random_state : int, default=0 Seed. stratify : None Not supported; any other value raises ``ValueError``. sample_weight : None Not supported; any other value raises ``ValueError``. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default=None Numeric mode (``'fast'`` is Apple only); ``None`` uses the package default. Returns ------- array or list of arrays ``None`` when no arrays are given. Raises ------ ValueError If the arrays have different lengths, or ``stratify`` or ``sample_weight`` is given. Examples -------- >>> import numpy as np, mojolearn >>> X = np.random.default_rng(0).random((1000, 8), dtype=np.float32) >>> y = np.arange(1000, dtype=np.float32) >>> Xb, yb = mojolearn.resample.resample(X, y, n_samples=500) """ where = "resample" if stratify is not None: raise ValueError(f"mojolearn {where}: stratify= is refused by name (resample/NOT_IMPLEMENTED.tsv): " "scikit-learn's per-class allocation (_approximate_mode) breaks ties with its RNG stream") if sample_weight is not None: raise ValueError(f"mojolearn {where}: sample_weight= is refused by name (resample/NOT_IMPLEMENTED.tsv): " "a weighted draw is an inverse-CDF lookup over a float cumulative sum not yet pinned") if not arrays: return None n = len(arrays[0]) for a in arrays[1:]: # glue: checks each argument array length if len(a) != n: raise ValueError(f"mojolearn {where}: Found input variables with inconsistent numbers of samples: " f"{[len(x) for x in arrays]}") count = n if n_samples is None else _int(n_samples, "n_samples", where) if not replace: if count < 0 or count > n: # Preserve existing refusal before requesting native buffers. resample_indices(n, n_samples, replace, random_state, numeric_mode) gathered = _gpu_gather(arrays, n, count, _int(random_state, "random_state", where), numeric_mode, replace=False) if gathered is not None: return gathered[0] if len(gathered) == 1 else gathered if replace: gathered = _gpu_gather(arrays, n, count, _int(random_state, "random_state", where), numeric_mode) if gathered is not None: return gathered[0] if len(gathered) == 1 else gathered gathered = _narrow_gather(arrays, n, count, _int(random_state, "random_state", where), numeric_mode) if gathered is not None: return gathered[0] if len(gathered) == 1 else gathered idx = resample_indices(n, n_samples, replace, random_state, numeric_mode) out = [_take(a, idx) for a in arrays] # glue: dispatches one gather per argument array return out[0] if len(out) == 1 else out
__all__ = ["bootstrap", "permutation_test", "monte_carlo_integrate", "resample", "resample_indices", "BootstrapResult", "PermutationTestResult", "MonteCarloResult", "STATISTICS", "METHODS", "ALTERNATIVES", "INTEGRANDS"]