生成随机数使它们的和等于预定值

44

所以这是问题:我想(例如)生成4个伪随机数,相加起来等于40。如何在Python中实现?我可以生成1-40之间的随机数,然后生成另一个介于余数和1之间的数字,等等,但那么第一个数字会有更大的机会“抓住”更多。

8个回答

122

这是标准解决方案。它类似于Laurence Gonsalves的答案,但比那个答案有两个优点。

  1. 它是统一的:每个由四个正整数相加得到40的组合在此方案中都是等可能出现的。

并且

  1. 它很容易适应其他总数(例如,7个数字相加达到100等)。
import random

def constrained_sum_sample_pos(n, total):
    """Return a randomly chosen list of n positive integers summing to total.
    Each such list is equally likely to occur."""

    dividers = sorted(random.sample(range(1, total), n - 1))
    return [a - b for a, b in zip(dividers + [total], [0] + dividers)]

示例输出:

>>> constrained_sum_sample_pos(4, 40)
[4, 4, 25, 7]
>>> constrained_sum_sample_pos(4, 40)
[9, 6, 5, 20]
>>> constrained_sum_sample_pos(4, 40)
[11, 2, 15, 12]
>>> constrained_sum_sample_pos(4, 40)
[24, 8, 3, 5]

解释:存在一个一一对应的关系,使得正整数4元组(a, b, c, d)满足a + b + c + d == 40,与整数三元组(e, f, g),其中0 < e < f < g < 40相对应。可以使用random.sample轻松产生后者。该对应关系在一个方向上由(e, f, g) = (a, a + b, a + b + c)给出,在反向上由(a, b, c, d) = (e, f - e, g - f, 40 - g)给出。

如果你想要非负整数(即允许0),而不是正整数,则有一个简单的转换方法:如果(a, b, c, d)是非负整数,且它们的和为40,那么(a+1, b+1, c+1, d+1)是正整数,它们的和为44,反之亦然。运用这个思路,我们有:

def constrained_sum_sample_nonneg(n, total):
    """Return a randomly chosen list of n nonnegative integers summing to total.
    Each such list is equally likely to occur."""

    return [x - 1 for x in constrained_sum_sample_pos(n, total + n)]

感谢 @FM 提供的 constrained_sum_sample_pos(4, 10) 的图形说明。以下是稍作编辑后的内容。

0 1 2 3 4 5 6 7 8 9 10  # The universe.
|                    |  # Place fixed dividers at 0, 10.
|   |     |       |  |  # Add 4 - 1 randomly chosen dividers in [1, 9]
  a    b      c    d    # Compute the 4 differences: 2 3 4 1

1
+1 这很有启发性--谢谢。我编辑了你的答案,添加了一个图形说明,帮助我理解算法。通常情况下,我会不愿意这样做,但我认为其他人可能会觉得有用。请随意更改或撤销我的编辑。 - FMc
5
如果需要限制生成的整数大于特定的值“low”,可以通过将“a-b”替换为“a-b +(low-1)”来实现,并通过将两个“total”实例替换为“total - (min-1)n”来补偿新总和中“n(low-1)”的增加。我还没有想出添加“high”阈值的方法。 - Jonas Lindeløv
1
高阈值有什么进展吗? - getglad
这个答案写得非常好,非常清晰明了。我不明白为什么这不是被采纳的答案。我认为很多人都在寻找这个答案。 - Shaun Han
1
@JonasLindeløv,谢谢你的补充!只是提醒一下,在术语“total - (min-1)*n”中,我相信你是指“total - (low-1)*n”。 - undefined
显示剩余3条评论

21

使用多项分布

from numpy.random import multinomial
multinomial(40, [1/4.] * 4)

在这个例子中,每个变量都将分布为二项式分布,其平均值n * p等于40*1/4=10


3
这显然是最干净、最结实的解决方案,但也许在回答中加入一些解释会有助于问题提问者理解为什么这是最佳答案。 - Hamman Samuel
这似乎会产生接近相等的值,而不是所需范围内的任意值:multinomial(2**16, [1/3] * 3)/2**16 -> array([0.33073425, 0.33273315, 0.33653259])(多次运行给出类似的结果)。在我看来,它看起来并不均匀。 - kram1032
那不是我的抱怨。总和确实正确。问题在于样本的一致性。它们会紧密地悬停在均匀分割的间隔上,而不是有时给出一些更大或更小的样本。最受欢迎的答案确实解决了这个问题。 - kram1032
@kram1032,它们不是均匀的,它们是二项式的,平均值为n * p,在您的情况下为1/3 * 2**16 ~ 21k。OP并没有要求均匀性。 - Ruggero Turra
没错,的确,没有要求统一性。 - kram1032

13
b = random.randint(2, 38)
a = random.randint(1, b - 1)
c = random.randint(b + 1, 39)
return [a, b - a, c - b, 40 - c]

(我假设您想要整数,因为您说“1-40”,但这很容易推广到浮点数。)

这是它的工作原理:

  • 随机将总范围分成两半,得到 b。奇怪的范围是因为中点以下至少有 2 个值,中点以上也是如此。 (这来自每个值的最小值为 1)。
  • 随机将每个范围再次分成两部分。再次进行边界设置以考虑最小值为 1。
  • 返回每个切片的大小。它们将加起来达到 40。

3
我认为你需要使用a = random.randint(1, b-1)c = random.randint(b+1, 39) 来确保在输出列表中不会得到零。此外,这具有稍微特殊的分布:形如 [1, 1, x, 38-x] 的结果出现的概率比均匀分布要高得多。 - Mark Dickinson
@Mark:我相信你是正确的。我在那里有几个偏移一个的错误。 - Laurence Gonsalves

8

生成4个随机数,计算它们的和,将每个数除以和再乘以40。

如果你想要整数,那么这需要一些非随机性。


这将创建一个非均匀分布。https://dev59.com/iWsz5IYBdhLWcg3wNE1q#8068956 - n1000

3
在范围[1,37]内(允许重复),只有37^4 = 1,874,161种四个整数的排列方式。枚举它们,保存并计算加起来为40的排列方式(这将是一个更小的数字N)。
在区间[0,N-1]中均匀地随机选择整数K,并返回第K个排列。可以很容易地看出,这保证了可能结果空间上的均匀分布,每个序列位置具有相同的分布。 (我看到的许多答案将最后一次选择偏低于前三次!)

这个答案不太适用于一般情况。 - Richard

1

这是对@Mark Dickinson版本的一种谦虚的改进,允许生成的整数包含零(使它们非负,而不是正数):

import random

def constrained_sum_sample_pos(n, total):
    """Return a randomly chosen list of n non-negative integers summing to total.
    Each such list is equally likely to occur."""

    dividers = sorted(random.choices(range(0, total), k=n-1))
    return [a - b for a, b in zip(dividers + [total], [0] + dividers)]

random.choices() 函数进行有放回的抽样,与 random.sample() 不同,后者是无放回的抽样。这个函数从 Python 3.6 开始新增。


1
有趣!OP没有指定他们想要什么分布,但值得注意的是,在这个解决方案中,分布可能有点奇怪,特别是对于小的n。例如,当n = 3total = 2时,有六个解:([0, 0, 2], [0, 2, 0], [2, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0])。看起来,这段代码大约一半的时间会给出[0, 1, 1],而[0, 0, 2][1, 0, 1]每个都有四分之一的概率。 - Mark Dickinson

0
如果您想要真正的随机性,请使用以下代码:
import numpy as np
def randofsum_unbalanced(s, n):
    # Where s = sum (e.g. 40 in your case) and n is the output array length (e.g. 4 in your case)
    r = np.random.rand(n)
    a = np.array(np.round((r/np.sum(r))*s,0),dtype=int)
    while np.sum(a) > s:
        a[np.random.choice(n)] -= 1
    while np.sum(a) < s:
        a[np.random.choice(n)] += 1
    return a

如果您想要更高的一致性水平,那么可以利用多项式分布:
def randofsum_balanced(s, n):
    return np.random.multinomial(s,np.ones(n)/n,size=1)[0]

-1

@markdickonson的基础上,通过提供一些控制因子之间分配的方法,我引入了方差/抖动作为每个因子之间均匀距离的百分比。

 def constrained_sum_sample(n, total, variance=50):
    """Return a random-ish list of n positive integers summing to total.

    variance: int; percentage of the gap between the uniform spacing to vary the result.
    """
    divisor = total/n
    jiggle = divisor * variance / 100 / 2
    dividers = [int((x+1)*divisor + random.random()*jiggle) for x in range(n-1)]
    result = [a - b for a, b in zip(dividers + [total], [0] + dividers)]
    return result

示例输出:

[12, 8, 10, 10]
[10, 11, 10, 9]
[11, 9, 11, 9]
[11, 9, 12, 8]

这个想法是将人口平均分配,然后在给定范围内随机向左或向右移动它们。由于每个值仍然绑定到统一点,我们不必担心它漂移。

对于我的目的来说已经足够好了,但并不完美。例如:第一个数字总是会变得更高,而最后一个数字总是会变得更低。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接