Writing
Memory efficient numpy.random.choice()
NumPy’s numpy.random.choice() method samples items at random from an array.
Unfortunately, sampling without replacement an int from
import numpy as np
N = 1_000_000_000_000
np.random.choice(N, size=2, replace=False)
This is because numpy.random.choice() creates (at least up to NumPy version 1.24) an array of size
def choice(self, a, size=None, replace=True, p=None):
...
# pop_size == a when `a` is an `int`
idx = self.permutation(pop_size)[:size]
...
if a.ndim == 0:
return idx
def permutation(self, object x):
...
if isinstance(x, (int, np.integer)):
# this creates an array of size `x`
arr = np.arange(x)
self.shuffle(arr)
return arr
However, intuitively we shouldn’t need to create an array of size
We could simply keep sampling with replacement (which doesn’t require
This very basic and straightforward algorithm would be defined as follows:
Algorithm 1 An algorithm to draw without replacement.
Require:
1:
2: while
3:
4: if
5:
6: end if
7: end while
Which in code looks like,
def choice(N: int, k: int = 1, replace: bool = True, p=None):
"""Sample `k` elements from 0 to `N-1` with or without replacement."""
if not replace and N >= 3.414213562373095 * k:
X = [np.random.choice(N, size=1, replace=True, p=p)]
X = set(X)
while len(X) != k:
s = np.random.choice(N, size=1, replace=True, p=p)
X.add(s)
return X
else:
return np.random.choice(N, size=k, replace=replace, p=p)
But why require that
Lemma 1 Algorithm 1 has a space complexity of
Proof. Both
This one was very simple. But, how about time complexity?
Lemma 2 Algorithm 1 has an expected time complexity of
Proof. In order to advance in the while loop, we need to add new elements to
Using this probability bound, the expected number of items drawn is:
Since we have to draw 1 item in line 1 of the algorithm and then we have to draw
We now note that the algorithm requires that
Note that sets in Python have
However, this is not all. We have used the fact that
Lemma 3 If
Proof.
Since,
With this lemma we can prove the following lemma:
Lemma 4 Algorithm 1 draws O(2k) items w.p. (with probability) less than
Proof. We define
So that, using Lemma 3,
Now, we set
Note that this is true since Lemma 3 still applies when
Thus finally,
We can now apply our algorithm requirement
Thus, in the worst case we always draw
This is very useful. It means that if
This is definitely better than the original algorithm, which when creating the array of length
To end this little exploration, note that in general if we had chosen
For example, the algorithm is
Example use-case
But, when do we have a massive
Let me present the original problem I was trying to solve which required me to create my own choice function: a memory-efficient implementation to sample without replacement itertools.product().
Let’s say that you have multiple iterables that can be indexed in Python and you want to sample
Since the space of all permutations is prohibitively large, we cannot simply generate all permutations first and then sample
The solution is to use our
def random_product(*iterables, size: int = None) -> tuple:
"""Memory efficient way to sample ``itertools.product`` at random without replacement."""
lens = [len(it) for it in iterables]
# N is usually extremely large!
N = np.prod(lens)
if size is None:
size = N
idx = np.unravel_index(choice(N, k=size, replace=False),
shape=lens)
idx = np.array(idx).T
for i_ in idx:
yield tuple(iterables[i][i_[i]] for i in range(len(iterables)))