Cristian Campos
Cristian Campos

Reputation: 57

Are there other ways to obtain a value from 2 ranges?

I am learning Python, and I would like to know if there are other functions, methods, or techniques to obtain a value from two ranges. I provide the only example I could came up with with my current knowledge. Any leads are quite appreciated:

import random

def choose_random_angle():
    range1 = list(range(1, 76))  
    range2 = list(range(120, 231)) 
    combined_ranges = range1 + range2  
    return random.choice(combined_ranges)  

I was expecting to be able to include both ranges, separated by ";" or something along the lines. However, after reviewing the documentation, range function can only contain start, end, and step. The solution I have works, but I am sure there is a more efficient way.

Upvotes: 4

Views: 158

Answers (5)

Lewis
Lewis

Reputation: 834

Testing a couple of solutions

import time
import random
import itertools


def test_function_efficiency(func, *args, num_iterations=10000):
    start_time = time.perf_counter()
    for _ in range(num_iterations):
        func(*args)
    end_time = time.perf_counter()
    total_time = end_time - start_time
    average_time = total_time / num_iterations
    return average_time


# tuples with ranges
range1 = (1, 76, 1)     # tuple(start, end, step)
range2 = (120, 231, 1)


def your_function(r1, r2):
    return random.choice(list(range(*r1)) + list(range(*r2)))

def using_itertools_chain(r1, r2):
    return random.choice(list(itertools.chain(range(*r1), range(*r2))))

# select a range and then choose a random number within that range
def direct_choose(r1, r2):
    return random.randrange(*random.choice((r1, r2)))   # equal weights

def direct_choose_weighted(r1, r2):
    size_r1 = r1[1] - r1[0] if len(r1) == 3 and r1[2] == 1 else len(range(*r1))
    size_r2 = r2[1] - r2[0] if len(r2) == 3 and r2[2] == 1 else len(range(*r2))
    weights = [size_r1, size_r2]
    return random.randrange(*random.choices([r1, r2], weights=weights)[0])   # weighted by range size
    
def direct_choose_multiple_ranges(*ranges):   # accept multiple range tuples
    return random.randrange(*random.choice(ranges))   # not weighted


# Test the functions
avg_time_your_function = test_function_efficiency(your_function, range1, range2)
avg_time_itertools_chain = test_function_efficiency(using_itertools_chain, range1, range2)
avg_time_direct = test_function_efficiency(direct_choose, range1, range2)
avg_time_direct_weighted = test_function_efficiency(direct_choose_weighted, range1, range2)
avg_time_ranges = test_function_efficiency(direct_choose_multiple_ranges, range1, range2)

print(f"Average time (your function):                 {avg_time_your_function * 1000:.8f} milliseconds")
print(f"Average time (itertools chain):               {avg_time_itertools_chain * 1000:.8f} milliseconds")
print(f"Average time (direct choose):                 {avg_time_direct * 1000:.8f} milliseconds")
print(f"Average time (direct choose weighted):        {avg_time_direct_weighted * 1000:.8f} milliseconds")
print(f"Average time (direct choose multiple ranges): {avg_time_ranges * 1000:.8f} milliseconds")


Got these results

range1 = (1, 76, 1)
range2 = (120, 231, 1)

Average time (your function):                 0.01118883 milliseconds
Average time (itertools chain):               0.01489987 milliseconds
Average time (direct choose):                 0.00362538 milliseconds
Average time (direct choose weighted):        0.01429519 milliseconds
Average time (direct choose multiple ranges): 0.00363713 milliseconds


range1 = (1, 1000, 2)
range2 = (1200, 1500, 1)
    
Average time (your function):                 0.05656854 milliseconds
Average time (itertools chain):               0.05776611 milliseconds
Average time (direct choose):                 0.00399395 milliseconds
Average time (direct choose weighted):        0.01802900 milliseconds
Average time (direct choose multiple ranges): 0.00419025 milliseconds

With bigger range sizes, direct selection, whether weighted or not, seems to be the most effective method.

Upvotes: 0

ThomasIsCoding
ThomasIsCoding

Reputation: 102529

You can use random.binomialvariate (added in version 3.12.)

import random
r1 = range(1, 76)
r2 = range(120, 231) 
p = len(r2)/(len(r1)+len(r2))
random.choice([r1,r2][random.binomialvariate(1, p)])

Upvotes: 1

blhsing
blhsing

Reputation: 107015

@MarkTolonen's answer is great for the specific case in the question but could use a bit of generalization for any two given ranges:

import random

def choose_random_angle(range1, range2):
    border = len(range1)
    pick = random.randrange(border + len(range2))
    if pick < border:
        picked_range = range1
    else:
        pick -= border
        picked_range = range2
    return picked_range[pick]

print(choose_random_angle(range(1, 76), range(120, 231)))

Upvotes: 1

no comment
no comment

Reputation: 10465

You could combine them once:

import random
import functools

choose_random_angle = functools.partial(
    random.choice,
    (*range(1, 76), *range(120, 231))
)

Attempt This Online!

Upvotes: 1

Mark Tolonen
Mark Tolonen

Reputation: 178021

For this specific example, you could use a single range, and skip the gap, e.g.:

import random

def choose_random_angle():
    a = random.randint(1, 186)
    return a if a < 76 else a + 44

So 1-75 returns 1-75, and 76-186 returns 120-230.

This would be more efficient then generating a large list of numbers.

Upvotes: 6

Related Questions