15

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

My implementation does not support uniform distribution. :(

Andrea Spadaccini
  • 12,378
  • 5
  • 40
  • 54
foobarbazmeow
  • 198
  • 1
  • 2
  • 9

9 Answers9

28

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.

Simen S
  • 3,210
  • 18
  • 24
  • 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 rounding – Marc Gravell Apr 06 '11 at 09:03
  • 1
    @Simen S Thanks a ton dude... you just made maths so easy – P R J May 18 '15 at 13:01
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

  • 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). – Oben Sonne Jul 20 '11 at 18:43
  • 1
    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. – Anirvan Mukherjee Dec 09 '11 at 23:24
  • 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. – RabbitBones22 Apr 18 '23 at 20:13
  • 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. – RabbitBones22 Apr 19 '23 at 10:47
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
Daren Thomas
  • 67,947
  • 40
  • 154
  • 200
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).

soakley
  • 233
  • 11
  • 15
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
litepresence
  • 3,109
  • 1
  • 27
  • 35
1

UPDATE

  1. Create a vector3 of 3 random numbers
  2. Normalize the vector
Marnix
  • 6,384
  • 4
  • 43
  • 78
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
vhallac
  • 13,301
  • 3
  • 25
  • 36
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
litepresence
  • 3,109
  • 1
  • 27
  • 35
  • Same comment as the other on this exact question. This question is tagged C#, please answer in the language for the question. – BradleyDotNET Sep 05 '14 at 01:18
  • 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. – litepresence Sep 05 '14 at 01:21
  • 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. – BradleyDotNET Sep 05 '14 at 01:29
  • 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))]` – litepresence Sep 12 '14 at 01:17
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

Loyalar
  • 2,131
  • 4
  • 24
  • 44