Lightweight utility to pick random items from a weighted list, with probability proportional to each item's weight. Zero external deps. MIT licensed.
- Lightweight & fast: zero dependencies, tree-shakable
- Typed: full TypeScript types
- Flexible: sampling with/without replacement, pluggable RNG
- Efficient: optimized picker via Alias method or CDF
npm install random-weighted-pick- Node:
>=16 - Supports ESM and CommonJS
// ESM
import weightedPick, { pickMany, createWeightedPicker } from 'random-weighted-pick'
// CommonJS
// const { weightedPick, pickMany, createWeightedPicker } = require('random-weighted-pick')
// or: const weightedPick = require('random-weighted-pick')const options = [
{ id: 0, weight: 0.2, item: () => 'Lemon' },
{ id: 1, weight: 0.3, item: ['Grape', 'Orange', 'Apple'] },
{ id: 2, weight: 0.4, item: 'Mango' },
{ id: 3, weight: 0.1, item: 3 },
]
// Weights are automatically normalized (sum does not need to be 1)
const one = weightedPick(options)
console.log(one) // { id: 2, item: 'Mango' }
// Pick multiple (with replacement by default)
const many = pickMany(options, 2)
// Pick multiple without replacement
const noReplacement = pickMany(options, 2, { replacement: false })
// Efficient picker for many selections (alias method by default)
const picker = createWeightedPicker(options, { method: 'alias' })
const next = picker.pick() // { id, item }Pick 1 item according to its weight.
Pick k items. When replacement: false, sampling is without replacement.
Create an optimized picker for multiple selections.
Returns an object with:
pick(): { id: number, item: T }pickMany(k: number): Array<{ id: number, item: T }>updateWeight(id: number, weight: number): void— updates an item's weight and rebuilds internal structures
export interface WeightedInput<T> {
id?: number
weight: number
item: T
}
export interface WeightedResult<T> {
id: number
item: T
}
export type RNG = () => number
export interface PickConfig {
normalize?: boolean // default: true
epsilon?: number // default: 1e-12
rng?: RNG // default: crypto.getRandomValues (when available) or Math.random
}
export type Method = 'cdf' | 'alias'pickManyalso accepts{ replacement?: boolean }.createWeightedPickeraccepts{ method?: 'cdf' | 'alias' }.
- Normalization:
normalize: trueby default. Weights are normalized to sum to 1. Ifnormalize: false, the sum of weights must be 1 (±epsilon). - RNG: defaults to
crypto.getRandomValueswhen available; otherwiseMath.random. You can inject a custom RNG. - Method:
aliasis recommended for many fast selections;cdfuses binary search over the CDF.
weightedPick(implicit CDF scan): O(n) to accumulate + O(n) worst-case scan; fast for small lists.createWeightedPickerwithalias:- Build: O(n)
pick(): O(1)
createWeightedPickerwithcdf:- Build: O(n)
pick(): O(log n) via binary search
pickMany(..., { replacement: false }): uses weighted random keys (Efraimidis–Spirakis). Generate keys, sort, take topk— O(n log n).
- Let raw weights be
w[i] >= 0. Whennormalize: true(default), probabilities arep[i] = w[i] / sum(w). - If
normalize: false, the library expects|sum(w) - 1| <= epsilonand throws otherwise. epsilon(default1e-12) is used to tolerate floating‑point rounding when checkingsum(w).- Items with
weight = 0are never selected. If all weights are zero, an error is thrown.
- Compute normalized probabilities
p[0..n-1]. - Draw
rfrom RNG (0 <= r < 1). If not, throw. - Find the smallest index
jwhere cumulative sumS[j] = p[0] + ... + p[j]satisfiesr < S[j].- Intuition: the unit interval is partitioned into segments of lengths
p[i]and we land whererfalls.
- Intuition: the unit interval is partitioned into segments of lengths
- Build the cumulative distribution array
cdfonce. - For each pick: draw
rin[0, 1), then binary‑search the smallest indexjwithr < cdf[j]. - Complexity: build
O(n), pickO(log n).
Build step (once):
- Normalize probabilities
p[i]and scale byn:q[i] = p[i] * n. - Split indices into
small = { i | q[i] < 1 }andlarge = { i | q[i] >= 1 }. - While both sets are non‑empty:
- Pop
lfromsmallandgfromlarge. - Set
prob[l] = q[l]andalias[l] = g. - Update
q[g] = q[g] + q[l] - 1and movegbetween sets accordingly.
- Pop
- Set remaining
prob[i] = 1for indices left in either set.
Pick step (each selection):
- Draw
r1andr2in[0, 1). - Compute
i = floor(r1 * n). - If
r2 < prob[i]picki, else pickalias[i].
Minimal numerical example (2 items): weights [0.25, 0.75], n=2 → scaled q = [0.5, 1.5].
small = [0],large = [1]→ setprob[0] = 0.5,alias[0] = 1; remainingprob[1] = 1.- When
i=0,r2 < 0.5selects0; otherwise selects1via alias.
To select k items without replacement, the library uses weighted random keys:
- For each item
iwith weightw[i] > 0, drawu[i]with0 < u[i] < 1. - Compute
key[i] = u[i]^(1 / w[i]). Largerkeymeans higher priority. - Sort by
keydescending and return the top‑kitems.
Notes:
- This requires a strict
0 < u < 1(not including endpoints). The library validates this and throws if violated. - Equivalent form using logs:
key = exp((ln u) / w), which is numerically similar but the direct power is used here.
- By default, the RNG is
crypto.getRandomValues(when available) orMath.random. - You can inject a custom RNG via
PickConfig.rng. Tests become deterministic by cycling through a fixed array of values.
- CDF pick with weights
[2, 1, 1]:- Normalized
p = [0.5, 0.25, 0.25]andS = [0.5, 0.75, 1]. r = 0.10→r < 0.5→ pick index0.r = 0.60→0.5 <= r < 0.75→ pick index1.r = 0.90→0.75 <= r < 1→ pick index2.
- Normalized
- Alias pick with
[0.25, 0.75](as above):prob = [0.5, 1],alias[0] = 1.r1 = 0.0 → i = 0,r2 = 0.4 < 0.5→ pick0.r1 = 0.0 → i = 0,r2 = 0.9 >= 0.5→ pickalias[0] = 1.
- CDF (Cumulative Distribution Function): for a discrete distribution with probabilities
p[i], the CDF at indexjisS[j] = p[0] + ... + p[j]. Sampling with a uniformr \in [0,1)returns the smallestjsuch thatr < S[j]. - PMF (Probability Mass Function): the set of probabilities
p[i]assigned to discrete outcomes. Here,p[i]comes from normalized weights. - Alias method (Vose/Walker):
O(1)sampling scheme that preprocesses probabilities into two tables (probandalias) so each draw is constant time. - RNG (Random Number Generator): function returning floats in
[0, 1). Defaults tocrypto.getRandomValues(when available) orMath.random, and can be injected viaPickConfig.rng. - Normalization: scaling raw weights
w[i]to probabilitiesp[i] = w[i] / sum(w)so they sum to 1. - Epsilon (
epsilon): small tolerance used when comparing floating‑point sums to 1 innormalize: falsemode. - Replacement: sampling with replacement allows the same item to appear multiple times; without replacement each selected item appears at most once.
- Weighted random keys (Efraimidis–Spirakis): method for sampling without replacement using
key = u^(1/w)withu \in (0,1), selecting the top‑kkeys.
let i = 0
const values = [0.1, 0.8, 0.4]
const rng = () => values[i++ % values.length]
const result = pickMany([
{ id: 0, weight: 1, item: 'A' },
{ id: 1, weight: 1, item: 'B' },
], 2, { rng })const picker = createWeightedPicker([
{ id: 0, weight: 1, item: 'A' },
{ id: 1, weight: 2, item: 'B' },
])
picker.pick()
picker.updateWeight(1, 5)
const batch = picker.pickMany(3)const res = pickMany([
{ id: 0, weight: 1, item: 'A' },
{ id: 1, weight: 1, item: 'B' },
{ id: 2, weight: 1, item: 'C' },
], 2, { replacement: false })- List must be an array of objects.
- Each item must have
weight(finite number ≥ 0) anditem. - If
normalize: false, the sum of weights must be 1 (±epsilon). - RNG must return a number
xin the range [0, 1), and for some internal cases (without replacement) it requires 0 < x < 1.
- TypeScript types included (
typesin the ESM distribution). - ESM and CJS exports configured via
package.json(exports,import/require). - Works in browsers when bundled (uses
crypto.getRandomValueswhen available). For environments withoutcrypto, inject your own RNG.
Requirements: Node 16+
# install dependencies
npm install
# tests
npm test
# coverage
npm run coverage
# build (ESM and CJS)
npm run buildSee CONTRIBUTING.md for guidelines. Please ensure tests and build pass before opening a PR.
MIT © KossHackaton OneTeam