Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

wsample

wsample logo

CILicense: MIT

Weighted sampling (with and without replacement) and reservoir sampling over streams, in pure Python with zero dependencies.

What is weighted sampling?

Standard random sampling picks items with equal probability. Weighted sampling lets you assign an importance (weight) to each item, so high-weight items are drawn more often.

Without replacement -- each item can appear in the result at most once. Use this when you need a diverse but importance-aware subset: top-k document candidates by relevance score, stratified data samples, sketch algorithms over data streams.

With replacement -- an item can be drawn multiple times. Use this when each draw is independent: Monte Carlo simulation, bootstrapping, generating random sequences from a fixed distribution. The multinomial function summarizes the same process as a count vector.

Why this library?

  • Pure Python, zero dependencies. No NumPy, no SciPy. One import, works everywhere.
  • Streaming.weighted_reservoir and reservoir consume any iterable in a single pass without materializing the whole dataset in memory.
  • Correct algorithms. A-Res and A-ExpJ are the accepted standard for weighted reservoir sampling (Efraimidis and Spirakis 2006). Algorithm L is the standard for unweighted reservoir sampling (Li 1994).
  • Reproducible. The caller passes an explicit rng=random.Random(seed); no global state is touched.

Install

pip install wsample

Note: wsample has not yet been published to PyPI. Install from source: pip install git+https://github.com/amaar-mc/wsample.git

Quick start

importrandomfromwsampleimport (
weighted_sample_no_replacement,
weighted_reservoir,
reservoir,
alias_sampler,
sample_with_replacement,
multinomial,
)
rng=random.Random(42)
# Weighted sample without replacement from a list (A-Res)items= ["apple", "banana", "cherry", "date", "elderberry"]
weights= [1.0, 2.0, 5.0, 3.0, 1.0]
result=weighted_sample_no_replacement(items, weights, k=3, rng=rng)
# -> high-weight items like "cherry" appear more often; each item at most once# Weighted sampling with replacement -- k i.i.d. draws, duplicates allowedrng5=random.Random(42)
indices=sample_with_replacement([1.0, 2.0, 5.0, 3.0, 1.0], k=10, rng=rng5)
# -> list of 10 indices in [0, 4]; index 2 (weight 5) appears most often# Multinomial count vector -- same i.i.d. draws summarized as countsrng6=random.Random(42)
counts=multinomial([1.0, 2.0, 5.0, 3.0, 1.0], trials=1000, rng=rng6)
# -> list of 5 counts summing to 1000; counts[2] is largest# Streaming weighted reservoir (A-ExpJ) -- same distribution, works on any iterabledefgenerate_documents():
fordoc_id, scoreinenumerate([0.9, 0.1, 0.7, 0.4, 0.8]):
yielddoc_id, scorerng2=random.Random(0)
top_docs=weighted_reservoir(
generate_documents(),
weight_fn=lambdapair: pair[1],
k=2,
rng=rng2,
)
# Unweighted reservoir (Algorithm L) -- O(k(1+log(n/k))) RNG callsrng3=random.Random(0)
sample=reservoir(range(1_000_000), k=100, rng=rng3)
# Alias method for fast with-replacement draws from a fixed distributionrng4=random.Random(0)
draw=alias_sampler([1.0, 2.0, 3.0], rng=rng4)
index=draw() # -> 0, 1, or 2 proportionally

API

All sampling functions take rng as a keyword-only argument with no default. Pass random.Random(seed) for reproducibility.

weighted_sample_no_replacement(items, weights, *, k, rng) -> list

Efraimidis-Spirakis A-Res. Returns up to k items from items, sampled without replacement proportional to weights. Items with weight 0 are never selected. k is clamped to the number of positive-weight items.

Raises ValueError if k < 0, len(weights) != len(items), or any weight is negative.

weighted_reservoir(stream, weight_fn, *, k, rng) -> list

A-ExpJ streaming algorithm. Consumes stream in a single pass. weight_fn(item) must return a strictly positive float. Equivalent in distribution to weighted_sample_no_replacement on the materialized list.

Raises ValueError if k < 0 or weight_fn returns a non-positive value.

reservoir(stream, *, k, rng) -> list

Unweighted Algorithm L. Single pass, O(k(1 + log(n/k))) RNG calls. Returns exactly min(k, n) items chosen uniformly at random without replacement.

Raises ValueError if k < 0.

alias_sampler(weights, *, rng) -> Callable[[], int]

Builds a Vose alias table in O(n) time. Returns a callable that draws an index in [0, len(weights)) in O(1) time per call, with replacement, proportional to weights.

Raises ValueError if weights is empty, any weight is negative, or all weights are zero.

sample_with_replacement(weights, *, k, rng) -> list[int]

Draws k indices independently with probability proportional to weight. Each draw is i.i.d. so the same index can appear multiple times. Builds a Vose alias table in O(n) then performs k O(1) draws, for O(n + k) total. k must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or k < 1.

multinomial(weights, *, trials, rng) -> list[int]

Returns the count vector for trials i.i.d. weighted draws. counts[i] is the number of times index i was drawn; len(counts) == len(weights) and sum(counts) == trials. Implemented by tallying sample_with_replacement, so both functions produce identical output under the same seed. trials must be >= 1.

Raises ValueError if weights is empty, any weight is negative, all weights are zero, or trials < 1.

With replacement vs without replacement:sample_with_replacement and multinomial allow repeated draws from the same index. weighted_sample_no_replacement and weighted_reservoir guarantee each index appears at most once in the result. Choose without-replacement when you need a diverse subset; choose with-replacement when draws are independent (bootstrapping, simulation, Monte Carlo).

See also

examples/top_k_stream.py for a worked example of streaming top-k weighted selection.

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Weighted sampling without replacement (Efraimidis-Spirakis A-Res and A-ExpJ) and reservoir sampling over streams, in pure Python with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages