How to generate three random numbers, whose sum is 1?
Asked Answered
J

9

14

I need to generate 3 random numbers, the amount of which is equal to 1.

My implementation does not support uniform distribution. :(

Jay answered 6/4, 2011 at 8:57 Comment(5)
Have you tried this yourself? What is the problem? can you post some code?Pirzada
These three numbers cannot be drawn from an uniform distribution, as it implies independence between the samples and you instead have a clear constraint on the values (x+y+z=1).Blindworm
@Ian Ringrose: no, personal project.Jay
That's only two random numbersKnack
The answer by (Simen S) is incorrect, it over samples in center, see MATHWORLDSpeaks
K
27

Just get 3 random numbers and then calculate a factor which is 1 / [sum of your numbers]. Finally multiply each of the random numbers with that factor. The sum will be 1.

Kerf answered 6/4, 2011 at 9:1 Comment(2)
This is the most obviously uniform of the 3 approaches posted so far; Daren's might be uniform, but I would have to think; this one is obviously uniform. You might, however, have problems with roundingPalmapalmaceous
@Simen S Thanks a ton dude... you just made maths so easySensualist
M
12

This is actually a tricky question. First of all:
Daren's solution is not uniform because it does not support having two numbers > 1/3.
Simen's solution is not uniform assuming the "pick a random number" draws from a uniform distribution, but this is a bit more subtle. It is at least symmetric between the variables (i.e. the probability of [a, b, c] is the same as that of any permutation of that), but it heavily favors solutions closer to (1/3, 1/3, 1/3). Think about it this way by looking at extreme cases: (1/3, 1/3, 1/3) could have come from any (a, a, a), where a ranges from 0 through 1. (1, 0, 0), an equally valid triple, must come from (1, 0, 0).

One solution: The set of positive numbers that add to 1 form a n equilateral triangle in three-space, with coordinates (1,0,0), (0,1,0), (0,0,1). Extend that to a parallelogram -- e.g. by adding a point (1,1,-1) as the fourth point. This double's the area -- map the second area to the first, so that it suffices to pick a random point in this parallelogram.

The parallelogram can be sampled uniformly via (0,0,1) + A(1,0,-1) + B (0,1,-1), where A and B range uniformly from 0 to 1.

-A

Metagenesis answered 20/7, 2011 at 8:49 Comment(4)
Concerning Simen's solution, you say "(1, 0, 0), an equally valid triple, must come from (1, 0, 0)" .. I'd say this is wrong: any (a,0,0) for a in (0,1] would generate (1,0,0).Polyneuritis
Good catch, that was incorrect! Here's a better explanation: Imagine the cube [0,1]^3; we sample this cube uniformly, and then normalize to a sum of 1. The triangular region of valid normalized points (x+y+z=1; x,y,z>=0) is the post-normalization image of some set of points in the [0,1]^3 cube -- namely, all points in the cube that are also on the line connecting the origin to this point on the plane (the resulting intersection is a segment). But the segment through (1/3, 1/3, 1/3) is the cube diagonal (len rt(3)), while that through (1,0,0) has len 1, so the former is rt(3) times as likely.Metagenesis
Mapping the second area onto the original triangle is non-trivial. While this solution is correct, without the mapping, some results will include negative numbers. Please include an example of mapping.Protuberance
Figured it out after much work: if (x,y,z) where z < 0, scale the point by -1, then translate x and y with +1. In other words, multiply the three numbers by -1, then add 1 to the first and second number. Leave the last number - which is now positive - unchanged.Protuberance
P
3

Generate two random numbers between 0 and 1. Divide those each by 3. The third is the difference of 1 and the two random thirds:

void Main()
{
    Random r = new Random();
    double d1 = r.NextDouble() / 3.0;
    double d2 = r.NextDouble() / 3.0;
    double d3 = 1.0 - d1 - d2;
    System.Console.WriteLine(d1);
    System.Console.WriteLine(d2);
    System.Console.WriteLine(d3);
    System.Console.WriteLine(d1 + d2 + d3);
}

this outputs the following in LINQPad:

0.0514050276878934
0.156857372489847
0.79173759982226
1
Phenology answered 6/4, 2011 at 9:0 Comment(1)
It will not be uniformly distributed. The third number is greater than the others.Kittykitwe
G
1

UPDATE

  1. Create a vector3 of 3 random numbers
  2. Normalize the vector
Garneau answered 6/4, 2011 at 9:0 Comment(3)
@Marc: True, I will update this to another answer. But the total has to be 1, so this was my first thought.Garneau
Same as Simen's solution, isn't it?Polyneuritis
I'm not sure this is right. A vector of magnitude 1 does not necessarily have its components sum to one if I remember my math correctly.Buffum
S
1

Slight variation on Marnix' answer:

  1. Generate a random number a from [0,1]
  2. Generate two random numbers. x from [0,a] and y from [a,1]
  3. Set results as x, y-x, 1-y
Subacid answered 6/4, 2011 at 9:16 Comment(0)
J
1

There is an easy way to do this, but you need to be able to generate a uniform random number.

Let X be uniform on (0,2/3). If X < 1/3, let Y = X + 1/3. Otherwise let Y = X - 1/3. Let Z = 1 - X - Y.

Under this setup, X, Y, and Z will sum to 1, they will all have identical uniform (0, 2/3) marginal distributions, and all three pairwise correlations will be -(1/2).

Jaehne answered 3/10, 2013 at 18:50 Comment(0)
Y
1

1/2 methods:

  • Create a list of random numbers, each 0 to 1 of length PARTS.
  • Sum the list
  • Divide each element by the sum
  • Round each element
  • Account for floating point math by editing the first element

Sorry don't know C#, here's the python:

import random
import time

PARTS       = 5
TOTAL       = 10
PLACES      = 3

def random_sum_split(parts, total, places):

    a = []
    for n in range(parts):
        a.append(random.random())
    b = sum(a)
    c = [x/b for x in a]    
    d = sum(c)
    e = c
    if places != None:
        e = [round(x*total, places) for x in c]
    f = e[-(parts-1):]
    g = total - sum(f)
    if places != None:
        g = round(g, places)
    f.insert(0, g)

    log(a)
    log(b)
    log(c)
    log(d)
    log(e)
    log(f)
    log(g)

    return f   

def tick():

    if info.tick == 1:

        start = time.time()

        alpha = random_sum_split(PARTS, TOTAL, PLACES)

        log('********************')
        log('***** RESULTS ******')
        log('alpha: %s' % alpha)
        log('total: %.7f' % sum(alpha))
        log('parts: %s' % PARTS)
        log('places: %s' % PLACES)

        end = time.time()  

        log('elapsed: %.7f' % (end-start))

yeilds:

Waiting...
Saved successfully.
[2014-06-13 00:01:00] [0.33561018369775897, 0.4904215932650632, 0.20264927800402832, 0.118862130636748, 0.03107818050878819]
[2014-06-13 00:01:00] 1.17862136611
[2014-06-13 00:01:00] [0.28474809073311597, 0.41609766067850096, 0.17193755673414868, 0.10084844382959707, 0.02636824802463724]
[2014-06-13 00:01:00] 1.0
[2014-06-13 00:01:00] [2.847, 4.161, 1.719, 1.008, 0.264]
[2014-06-13 00:01:00] [2.848, 4.161, 1.719, 1.008, 0.264]
[2014-06-13 00:01:00] 2.848
[2014-06-13 00:01:00] ********************
[2014-06-13 00:01:00] ***** RESULTS ******
[2014-06-13 00:01:00] alpha: [2.848, 4.161, 1.719, 1.008, 0.264]
[2014-06-13 00:01:00] total: 10.0000000
[2014-06-13 00:01:00] parts: 5
[2014-06-13 00:01:00] places: 3
[2014-06-13 00:01:00] elapsed: 0.0054131
Ytterbium answered 5/9, 2014 at 1:15 Comment(1)
This is not C#. Please note the tags being used on the question.Buffum
Y
0

2/2 methods:

  • Create a list of random numbers 0 to 1; scaled to the total
  • Sort the list small to big
  • Create a new list by measuring the space between each element in the first list
  • Round each element in the new list
  • Replace first element to account for floating point

Sorry I don't know C# this is what it looks like in python:

import random
import time

PARTS       = 5
TOTAL       = 10
PLACES      = 3

def random_sum_split(parts, total, places):


    a = [0.0, total]
    for i in range(parts-1):
        a.append(random.random()*total)
    a.sort()
    b = []
    for i in range(1,(parts+1)):
        b.append(a[i] - a[i-1])
    if places != None:    
        b = [round(x, places) for x in b]  
    c = b[-(parts-1):]
    d = total - sum(c)
    if places != None:
        d = round(d, places)
    c.insert(0, d)

    log(a)
    log(b)
    log(c)
    log(d)

    return c

def tick():

    if info.tick == 1:

        start = time.time()

        alpha = random_sum_split(PARTS, TOTAL, PLACES)

        log('********************')
        log('***** RESULTS ******')
        log('alpha: %s' % alpha)
        log('total: %.7f' % sum(alpha))
        log('parts: %s' % PARTS)
        log('places: %s' % PLACES)

        end = time.time()  

        log('elapsed: %.7f' % (end-start))

Yields:

Waiting...
Saved successfully.
[2014-06-13 00:01:00] [0.0, 1.3005056784596913, 3.0412441135728474, 5.218388755020509, 7.156425483589107, 10]
[2014-06-13 00:01:00] [1.301, 1.741, 2.177, 1.938, 2.844]
[2014-06-13 00:01:00] [1.3, 1.741, 2.177, 1.938, 2.844]
[2014-06-13 00:01:00] 1.3
[2014-06-13 00:01:00] ********************
[2014-06-13 00:01:00] ***** RESULTS ******
[2014-06-13 00:01:00] alpha: [1.3, 1.741, 2.177, 1.938, 2.844]
[2014-06-13 00:01:00] total: 10.0000000
[2014-06-13 00:01:00] parts: 5
[2014-06-13 00:01:00] places: 3
[2014-06-13 00:01:00] elapsed: 0.0036860
Ytterbium answered 5/9, 2014 at 1:12 Comment(4)
Same comment as the other on this exact question. This question is tagged C#, please answer in the language for the question.Buffum
oops sorry redirected from a python thread. The theory is still there; two alternative solutions are shown. I don't know C# but I'll edit/post a generic algorithm to go with it to aid in porting.Ytterbium
It could be formatted more nicely, and I don't think I could personally perform the conversion from the information here, but I appreciate the effort. I'll remove my downvotes.Buffum
In its most simple form:a = [0, total] + [random.random()*total for i in range(parts-1)] a.sort() b = [(a[i] - a[i-1]) for i in range(1, (parts+1))]Ytterbium
S
0

Building upon @Simen and @Daren Thomas' answers, here is a service function that returns a list of doubles with uniform random values, where you can specify how many numbers you want, the total sum and the amount of digits on the numbers:

        public static List<double> GetListOfRandomDoubles(int countOfNumbers, double totalSum, int digits)
        {
            Random r = new Random();

            List<double> randomDoubles = new List<double>();
            double totalRandomSum = 0; 

            for (int i = 0; i < countOfNumbers; i++)
            {
                double nextDouble = r.NextDouble();
                randomDoubles.Add(nextDouble);
                totalRandomSum += nextDouble;
            }

            double totalFactor = 1 / totalRandomSum;
            totalFactor = totalFactor * totalSum;

            for (int i = 0; i < randomDoubles.Count; i++)
            {
                randomDoubles[i] = randomDoubles[i] * totalFactor;
                randomDoubles[i] = Math.Round(randomDoubles[i], digits);
            }

            double currentRandomSum = 0;
            randomDoubles.ForEach(x => currentRandomSum += x);
            randomDoubles[0] += totalSum - currentRandomSum;

            return randomDoubles;
        }

Usage:

        // Get list of 7 random doubles that sum to 100, with up to 2 digits on each number
        List<double> randomDoubles = GetListOfRandomDoubles(7, 100, 2);

Returns:

12.25, 19.52, 15.49, 16.45, 1.92, 13.12, 21.25

Spake answered 12/5, 2020 at 8:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.