CS1999
CS1999

Reputation: 509

Generate 4 random numbers between -1.0 and 1.0 such their sum is 1 using python

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

Answers (5)

ThomasIsCoding
ThomasIsCoding

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

lastchance
lastchance

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:

  • change the range to [0,2] by having an intermediate variable Y=X+1; everything is then positive (or 0);
  • change the intended sum to 1+4 (or, in general, to S+n, where n is number of variables)
  • As Mark Dickinson does, I find the DIVIDERS - in this case by uniform random variables in the range 0 to S+n, subsequently sorted.
  • Then the intended Y values are the gaps between the dividers.
  • I have to do a final check that the largest gap, or value of Y, doesn't exceed 2 (and recurse if it does).
  • Finally, revert to X=Y-1
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

Axel Donath
Axel Donath

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

sakith umagiliya
sakith umagiliya

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

Bilakshan Purohit
Bilakshan Purohit

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

Related Questions