Reputation: 509
I am trying generate 4 random numbers between -1.0 and 1.0 such that their sum is 1 using python. I initially looked at the dirichlet function in numpy but that only works for positive numbers. One other way I can think of is:
def generate_random_numbers():
numbers = np.random.uniform(-1.0, 1.0, 3)
last_number = 1 - np.sum(numbers)
if -1.0 <= last_number <= 1.0:
return np.append(numbers, last_number)
else:
return generate_random_numbers()
However its not that efficient. Any other way to do this?
Upvotes: 8
Views: 236
Reputation: 102880
With your specific problem, you can artificially construct the "randomness" and make it constrained by summing to 1
. For example
def f(seed=0):
np.random.seed(seed)
while True:
v = np.append(x := np.random.normal([-.5, 0, .5], 0.5/3), 1 - np.sum(x))
if all(abs(v) <= 1):
np.random.shuffle(v)
return v
print(f"with seed = 0: {f()}\n")
print(f"with seed = 1: {f(1)}\n")
print(f"with seed = 2: {f(2)}\n")
such that you will obtain
with seed = 0: [ 0.663123 -0.20599128 0.47617541 0.06669287]
with seed = 1: [ 0.9192638 -0.22927577 -0.1019594 0.41197137]
with seed = 2: [ 0.09190901 -0.65150127 0.88203467 0.67755759]
Upvotes: 0
Reputation: 6805
This is massively indebted to Mark Dickinson's answer in the previously-linked thread (Generate random numbers summing to a predefined value ). However, this is different insofar as the answer requires floats (so random.sample won't work), involves negative numbers, and, as far as I can see, you would have to reject some groupings and recurse occasionally.
Basically, I:
import numpy as np
def generate_random_numbers( S, n ):
dividers = np.concatenate( ( [0], np.sort( np.random.uniform( 0.0, S + n, n - 1 ) ), [S+n] ) )
y = np.diff( dividers )
if np.max( y ) > 2: return generate_random_numbers( S, n )
return y - 1
print( generate_random_numbers( 1.0, 4 ) )
Sample output:
[ 0.69312962 -0.01597064 0.77286876 -0.45002774]
A bit of testing with more samples shows that the averages are OK (0.25 for each variable) but the rejection rate is annoyingly high.
Upvotes: 0
Reputation: 1723
From a quick study:
import numpy as np
random_state = np.random.RandomState(98238)
n_samples = 100_000
n_dim = 4
numbers = random_state.uniform(-1.0, 1.0, (n_samples, n_dim - 1))
last_number = 1 - np.sum(numbers, axis=1, keepdims=True)
is_valid = (-1.0 <= last_number) & (last_number <= 1.0)
samples = np.append(numbers, last_number, axis=1)[is_valid[:, 0]]
print(f"Acceptance ratio {is_valid.sum() / n_samples:.2f}")
Which gives:
Acceptance ratio 0.48
You can find that your acceptance ratio for the rejection sampling method you proposed is around 0.48. So on average the approach would be by a factor of ~2 worse compared to a perfect direct sampling method. This is not bad, given that the method is very simple.
I would suggest to keep your method and change to the vectorized version I showed above if you need more than a single sample.
Upvotes: 3
Reputation: 1
import numpy as np
def random_numbers_with_sum(n=4, target_sum=1.0, min_val=-1.0, max_val=1.0)
nums = np.random.uniform(min_val, max_val, n)
# Normalize to make them sum to target_sum
current_sum = np.sum(nums)
nums = nums - (current_sum - target_sum) / n
# Check if all values are still within bounds
if np.all((nums >= min_val) & (nums <= max_val)):
return nums
else:
# Try again if bounds are violated
return random_numbers_with_sum(n, target_sum, min_val, max_val)
# Generate and verify
nums = random_numbers_with_sum()
print(f"Numbers: {nums}")
print(f"Sum: {sum(nums)}")
print(f"All in range: {all(-1.0 <= x <= 1.0 for x in nums)}")
Upvotes: 0
Reputation: 26
As you have tagged your question with #numpy, I think this answer/program should be acceptable:
import random
def generate_numbers():
while True:
nums = [random.uniform(-1, 1) for _ in range(3)] # get 3 numbers
fourth_num = 1 - sum(nums) # compute 4th number
if -1 <= fourth_num <= 1: # append if it satisfies the condition
nums.append(fourth_num)
return nums
numbers = generate_numbers()
print(numbers, "Sum:", sum(numbers))
Note: the returned type is of Numpy's NDArray ;)
Upvotes: 0