Approach #1
For N
>> n
, we can use an iterative method with masking, so that at each iteration we pick one not-previously picked element per row. The implementation would look something like this -
R = np.arange(M)
mask = np.ones((M,N), dtype=bool)
idx = np.random.randint(0,N,(M))
mask[R,idx] = 0
for i in range(1,n):
lim = N-i
m2 = np.ones((M,lim), dtype=bool)
idx2 = np.random.randint(0,lim,(M))
m2[R,idx2] = 0
mask[mask] = m2.ravel()
out = np.nonzero(~mask)[1].reshape(-1,n)
If you need to randomize numbers per row, use the rand-trick as linked in question post :
out = np.take_along_axis(out, np.random.rand(M,n).argsort(1), axis=1)
If the constant array-creation with m2
bothers you, re-use after initializing before looping, while keeping the rest of the code same -
m2 = np.ones((M,N-1), dtype=bool)
for i in range(1,n):
lim = N-i
idx2 = np.random.randint(0,lim,(M))
m2[R,idx2] = 0
mask[mask] = m2.ravel()
m2[R,idx2] = 1
m2 = m2[:,:-1]
Approach #2 Similar to Approach #1
, but the initialization part does most of the job to setup unqiue random numbers per row. An additional while
iterative part takes care of the rows that could not assign unique ones. With N
>> n
, we will hardly need to iterate though. The implementation would look something like this -
# https://stackoverflow.com/a/51915131/ @Divakar
def random_num_per_grp(L):
# For each element in L pick a random number within range specified by it
r1 = np.random.rand(np.sum(L)) + np.repeat(np.arange(len(L)),L)
offset = np.r_[0,np.cumsum(L[:-1])]
return r1.argsort()[offset] - offset
R = np.arange(M)
mask = np.ones((M,N), dtype=bool)
idx = np.random.randint(0,N,(M,n))
mask[R[:,None],idx] = 0
rows_notdone = mask.sum(1)!=N-n
while np.any(rows_notdone):
idx0 = random_num_per_grp(mask[rows_notdone].sum(1))
steps = np.r_[0,mask.sum(1).cumsum()[:-1]]
flat_idx0 = steps[rows_notdone] + idx0
m2 = np.ones(mask.sum(), dtype=bool)
m2[flat_idx0] = 0
mask[mask] = m2
rows_notdone = mask.sum(1)!=N-n
out = np.nonzero(~mask)[1].reshape(-1,n)