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:

enter image description here

The same benchmark with generating 1000 pairs and the same ratio of areas, I got this:

enter image description here

Asked By: plpm

||

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.

Answered By: Samwise

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)
Answered By: Alain T.

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)

Demo : https://trinket.io/python3/f9e65fe9ef

Answered By: MatBailie
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.