How to efficiently generate unique random non-zero integers from specific spaces in a range?
Question:
I want to generate 100 pairs of unique random non-zero integers from the range (-150, 151). But I want my code to generate from specific areas in (-150, 151). I have coded it as follows:
import random
my_list = []
c = 0
while c < 100:
if c < 5:
# Both values are unique.
first_num, second_num = random.sample(range(-5, 6), 2)
# Exclusion of 0.
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-5, 6), 2)
c += 1
elif 5 <= c < 80:
first_num, second_num = random.sample(range(-100, 101), 2)
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-100, 101), 2)
c += 1
else:
first_num, second_num = random.sample(range(-150, 151), 2)
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-150, 151), 2)
c += 1
random_nums = (first_num, second_num)
if random_nums not in my_list:
my_list.append(random_nums)
else:
c -= 1
Is there a more elegant or efficient way alternative to what I have coded above?
UPDATE
After discussing with MatBailie in the comments, I am updating the requirements:
If the whole selected range is (-150 to 151), then in the final my_list
:
- (5,6), (6,5) are allowed but (5,6), (5,6) are not.
- If (-1, 1) is generated once from range (-5 to 6) then it should not be generated again from the bigger ranges such as (-100 to 101) or (-150 to 151)
- It is also good to have pairs such as (-1, 140) or (140, -1).
UPDATE
Benchmarking MatBailie, Alain T. and Samwise’s answers on perfpy with 100 pairs, I got the following result:
The same benchmark with generating 1000 pairs and the same ratio of areas, I got this:
Answers:
I might do it like this, using a list of numbers to be able to take the sample
without having to take an extra step of dropping the zeroes, and using a dict to enforce uniqueness (a set would be simpler, but a dict preserves insertion order, which is important if you want the same arrangement of the different samples in the final list, with smaller numbers toward the beginning):
import random
max_num = 150
nums = list(range(-max_num, max_num))
nums.remove(0)
my_list = list({
tuple(
random.sample(nums[max_num-r:r-max_num or None], 2)
): 0
for c, r in ((5, 5), (75, 100), (20, 150))
for _ in range(c)
})
Generating a single nums
list is a little more complicated than generating a new list for each sample population, but is more efficient since then we don’t need to repeatedly re-allocate it, and can simply take slices as needed.
Note that max_num
needs to be at least as big as all of the r
values in the for c, r
iteration, or we won’t be able to get the desired range by slicing nums
with max_num-r
etc.
You could build a list of the 100 ranges repeating the same ones in groups of the required size. This will make the rest of the logic more generalized and simpler:
import random
ranges = [range(-5,6)]*5 + [range(-100,101)]*75 + [range(-150,151)]*20
pairs = []
for R in ranges:
a = 0
while not a or not b or (a,b) in pairs:
a,b = random.sample(R,2) # random.choices(R,k=2)
pairs.append((a,b))
output:
print(pairs)
[(-3, 2), (-3, -5), (3, 1), (-4, -3), (2, 4), (-73, -9), (16, -56),
(12, -81), (5, -50), (99, -6), (35, 71), (30, -75), (98, 25), (55, -58),
(73, -24), (-65, 4), (75, -96), (-6, -90), (-80, 22), (-93, -39),
(-69, -48), (-30, 25), (85, -11), (37, 60), (91, 96), (98, 100),
(-100, -54), (58, 20), (-14, -95), (-76, -12), (-5, -84), (70, -53),
(91, -66), (-61, 2), (4, -42), (-15, -70), (-52, -6), (-5, 93), (26, 76),
(-8, -79), (63, -7), (-23, -27), (56, 13), (46, 27), (80, 94), (-94, -66),
(-46, -19), (-4, -87), (-92, -48), (24, 32), (10, -89), (-50, -96),
(-5, -85), (-32, 69), (-60, 69), (87, -81), (-100, -91), (87, 37),
(-60, 45), (-70, -84), (-98, 80), (-88, 57), (-67, -44), (-72, 4),
(-76, 5), (-79, -16), (-9, 80), (76, -41), (15, 77), (-47, -1), (58, 12),
(66, -2), (21, 16), (-24, -12), (54, -69), (58, -73), (61, 68), (-89, -37),
(-85, 68), (-7, -21), (-86, -31), (48, 136), (137, 86), (-61, -32),
(104, -144), (-24, -103), (80, 135), (-2, 39), (49, -21), (15, -103),
(-14, -145), (-48, -61), (142, -90), (93, -132), (-68, -111), (-80, 2),
(6, 45), (-89, 75), (-146, -76), (-94, -24)]
You could also do this using only one list by initializing the resulting list with ranges that you replace with the generated pairs:
pairs = [range(-5,6)]*5 + [range(-100,101)]*75 + [range(-150,151)]*20
for i,R in enumerate(pairs):
pairs[i] = (0,0)
while not all(pairs[i]) or pairs.index(pairs[i])<i:
pairs[i] = random.sample(R,2) # random.choices(R,k=2)
It’s gone past midnight, but I’ll do my best to explain…
The basic principle is that I work out how many different pairs there are in a given range (excluding any zeros), and run a sample on that range, so I know there are no duplicates.
Then I code a function to "decode" those integers in to a pairs.
So, if I want a pair with elements in the range -4 to +4, excluding zeros, excluding pairs from the range -2 to +2, I can map that to a grid of possible pairs…
-4 -3 -2 -1 +0 +1 +2 +3 +4
+4 O O O O X O O O O
+3 O O O O X O O O O
+2 O O X X X X X O O
+1 O O X X X X X O O
+0 X X X X X X X X X
-1 O O X X X X X O O
-2 O O X X X X X O O
-3 O O O O X O O O O
+4 O O O O X O O O O
O = Possible pair
X = Excluded pair
There are 48 allowed possible pairs.
- 48 = (4² – 2²) * 4
I then write a function that can turn each of the values 0..47 in to a different one of those allowed possible pairs.
import random
from pprint import pprint
def decode(val, lower, upper):
q = val % 4
v = val >> 2
x = v % (upper + lower) - lower
if x >= 0:
x += 1
y = v // (upper + lower) + lower + 1
if q in (1,3):
x = -x
y = -y
if q in (2,3):
x,y = -y,x
return (x,y)
def pairs(n, lower, upper):
return [
decode(id, lower, upper)
for id in random.sample(
range((upper**2 - lower**2) * 4),
n
)
]
# smaller ranges used to make it printable and testable
result = [
*pairs( 5, 0, 5), # 5 pairs with values in the range - 5 to 5, except pairs using the range - 0 to 0
*pairs( 5, 5, 10), # 5 pairs with values in the range -10 to 10, except pairs using the range - 5 to 5
*pairs( 5, 10, 20) # 5 pairs with values in the range -20 to 20, except pairs using the range -10 to 10
]
pprint(result)
I want to generate 100 pairs of unique random non-zero integers from the range (-150, 151). But I want my code to generate from specific areas in (-150, 151). I have coded it as follows:
import random
my_list = []
c = 0
while c < 100:
if c < 5:
# Both values are unique.
first_num, second_num = random.sample(range(-5, 6), 2)
# Exclusion of 0.
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-5, 6), 2)
c += 1
elif 5 <= c < 80:
first_num, second_num = random.sample(range(-100, 101), 2)
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-100, 101), 2)
c += 1
else:
first_num, second_num = random.sample(range(-150, 151), 2)
while first_num == 0 or second_num == 0:
first_num, second_num = random.sample(range(-150, 151), 2)
c += 1
random_nums = (first_num, second_num)
if random_nums not in my_list:
my_list.append(random_nums)
else:
c -= 1
Is there a more elegant or efficient way alternative to what I have coded above?
UPDATE
After discussing with MatBailie in the comments, I am updating the requirements:
If the whole selected range is (-150 to 151), then in the final my_list
:
- (5,6), (6,5) are allowed but (5,6), (5,6) are not.
- If (-1, 1) is generated once from range (-5 to 6) then it should not be generated again from the bigger ranges such as (-100 to 101) or (-150 to 151)
- It is also good to have pairs such as (-1, 140) or (140, -1).
UPDATE
Benchmarking MatBailie, Alain T. and Samwise’s answers on perfpy with 100 pairs, I got the following result:
The same benchmark with generating 1000 pairs and the same ratio of areas, I got this:
I might do it like this, using a list of numbers to be able to take the sample
without having to take an extra step of dropping the zeroes, and using a dict to enforce uniqueness (a set would be simpler, but a dict preserves insertion order, which is important if you want the same arrangement of the different samples in the final list, with smaller numbers toward the beginning):
import random
max_num = 150
nums = list(range(-max_num, max_num))
nums.remove(0)
my_list = list({
tuple(
random.sample(nums[max_num-r:r-max_num or None], 2)
): 0
for c, r in ((5, 5), (75, 100), (20, 150))
for _ in range(c)
})
Generating a single nums
list is a little more complicated than generating a new list for each sample population, but is more efficient since then we don’t need to repeatedly re-allocate it, and can simply take slices as needed.
Note that max_num
needs to be at least as big as all of the r
values in the for c, r
iteration, or we won’t be able to get the desired range by slicing nums
with max_num-r
etc.
You could build a list of the 100 ranges repeating the same ones in groups of the required size. This will make the rest of the logic more generalized and simpler:
import random
ranges = [range(-5,6)]*5 + [range(-100,101)]*75 + [range(-150,151)]*20
pairs = []
for R in ranges:
a = 0
while not a or not b or (a,b) in pairs:
a,b = random.sample(R,2) # random.choices(R,k=2)
pairs.append((a,b))
output:
print(pairs)
[(-3, 2), (-3, -5), (3, 1), (-4, -3), (2, 4), (-73, -9), (16, -56),
(12, -81), (5, -50), (99, -6), (35, 71), (30, -75), (98, 25), (55, -58),
(73, -24), (-65, 4), (75, -96), (-6, -90), (-80, 22), (-93, -39),
(-69, -48), (-30, 25), (85, -11), (37, 60), (91, 96), (98, 100),
(-100, -54), (58, 20), (-14, -95), (-76, -12), (-5, -84), (70, -53),
(91, -66), (-61, 2), (4, -42), (-15, -70), (-52, -6), (-5, 93), (26, 76),
(-8, -79), (63, -7), (-23, -27), (56, 13), (46, 27), (80, 94), (-94, -66),
(-46, -19), (-4, -87), (-92, -48), (24, 32), (10, -89), (-50, -96),
(-5, -85), (-32, 69), (-60, 69), (87, -81), (-100, -91), (87, 37),
(-60, 45), (-70, -84), (-98, 80), (-88, 57), (-67, -44), (-72, 4),
(-76, 5), (-79, -16), (-9, 80), (76, -41), (15, 77), (-47, -1), (58, 12),
(66, -2), (21, 16), (-24, -12), (54, -69), (58, -73), (61, 68), (-89, -37),
(-85, 68), (-7, -21), (-86, -31), (48, 136), (137, 86), (-61, -32),
(104, -144), (-24, -103), (80, 135), (-2, 39), (49, -21), (15, -103),
(-14, -145), (-48, -61), (142, -90), (93, -132), (-68, -111), (-80, 2),
(6, 45), (-89, 75), (-146, -76), (-94, -24)]
You could also do this using only one list by initializing the resulting list with ranges that you replace with the generated pairs:
pairs = [range(-5,6)]*5 + [range(-100,101)]*75 + [range(-150,151)]*20
for i,R in enumerate(pairs):
pairs[i] = (0,0)
while not all(pairs[i]) or pairs.index(pairs[i])<i:
pairs[i] = random.sample(R,2) # random.choices(R,k=2)
It’s gone past midnight, but I’ll do my best to explain…
The basic principle is that I work out how many different pairs there are in a given range (excluding any zeros), and run a sample on that range, so I know there are no duplicates.
Then I code a function to "decode" those integers in to a pairs.
So, if I want a pair with elements in the range -4 to +4, excluding zeros, excluding pairs from the range -2 to +2, I can map that to a grid of possible pairs…
-4 -3 -2 -1 +0 +1 +2 +3 +4
+4 O O O O X O O O O
+3 O O O O X O O O O
+2 O O X X X X X O O
+1 O O X X X X X O O
+0 X X X X X X X X X
-1 O O X X X X X O O
-2 O O X X X X X O O
-3 O O O O X O O O O
+4 O O O O X O O O O
O = Possible pair
X = Excluded pair
There are 48 allowed possible pairs.
- 48 = (4² – 2²) * 4
I then write a function that can turn each of the values 0..47 in to a different one of those allowed possible pairs.
import random
from pprint import pprint
def decode(val, lower, upper):
q = val % 4
v = val >> 2
x = v % (upper + lower) - lower
if x >= 0:
x += 1
y = v // (upper + lower) + lower + 1
if q in (1,3):
x = -x
y = -y
if q in (2,3):
x,y = -y,x
return (x,y)
def pairs(n, lower, upper):
return [
decode(id, lower, upper)
for id in random.sample(
range((upper**2 - lower**2) * 4),
n
)
]
# smaller ranges used to make it printable and testable
result = [
*pairs( 5, 0, 5), # 5 pairs with values in the range - 5 to 5, except pairs using the range - 0 to 0
*pairs( 5, 5, 10), # 5 pairs with values in the range -10 to 10, except pairs using the range - 5 to 5
*pairs( 5, 10, 20) # 5 pairs with values in the range -20 to 20, except pairs using the range -10 to 10
]
pprint(result)