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'
pickMany
also accepts{ replacement?: boolean }
.createWeightedPicker
accepts{ method?: 'cdf' | 'alias' }
.
- Normalization:
normalize: true
by default. Weights are normalized to sum to 1. Ifnormalize: false
, the sum of weights must be 1 (±epsilon
). - RNG: defaults to
crypto.getRandomValues
when available; otherwiseMath.random
. You can inject a custom RNG. - Method:
alias
is recommended for many fast selections;cdf
uses binary search over the CDF.
weightedPick
(implicit CDF scan): O(n) to accumulate + O(n) worst-case scan; fast for small lists.createWeightedPicker
withalias
:- Build: O(n)
pick()
: O(1)
createWeightedPicker
withcdf
:- 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| <= epsilon
and throws otherwise. epsilon
(default1e-12
) is used to tolerate floating‑point rounding when checkingsum(w)
.- Items with
weight = 0
are never selected. If all weights are zero, an error is thrown.
- Compute normalized probabilities
p[0..n-1]
. - Draw
r
from RNG (0 <= r < 1
). If not, throw. - Find the smallest index
j
where 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 wherer
falls.
- Intuition: the unit interval is partitioned into segments of lengths
- Build the cumulative distribution array
cdf
once. - For each pick: draw
r
in[0, 1)
, then binary‑search the smallest indexj
withr < 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
l
fromsmall
andg
fromlarge
. - Set
prob[l] = q[l]
andalias[l] = g
. - Update
q[g] = q[g] + q[l] - 1
and moveg
between sets accordingly.
- Pop
- Set remaining
prob[i] = 1
for indices left in either set.
Pick step (each selection):
- Draw
r1
andr2
in[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.5
selects0
; otherwise selects1
via alias.
To select k
items without replacement, the library uses weighted random keys:
- For each item
i
with weightw[i BE6A ] > 0
, drawu[i]
with0 < u[i] < 1
. - Compute
key[i] = u[i]^(1 / w[i])
. Largerkey
means higher priority. - Sort by
key
descending and return the top‑k
items.
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 indexj
isS[j] = p[0] + ... + p[j]
. Sampling with a uniformr \in [0,1)
returns the smallestj
such 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 (prob
andalias
) 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: false
mode. - 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‑k
keys.
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
x
in the range [0, 1), and for some internal cases (without replacement) it requires 0 < x < 1.
- TypeScript types included (
types
in the ESM distribution). - ESM and CJS exports configured via
package.json
(exports
,import
/require
). - Works in browsers when bundled (uses
crypto.getRandomValues
when 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 build
See CONTRIBUTING.md
for guidelines. Please ensure tests and build pass before opening a PR.
MIT © KossHackaton OneTeam