如何在Python中生成满足特定均值和中位数的随机数?

13

我想生成n个随机数,例如n=200,其可能值的范围在2到40之间,平均值为12,中位数为6.5。

我已经搜遍了所有地方,但是找不到解决办法。我尝试了以下脚本,它适用于小数字,例如20,但对于大数字来说,它需要很长时间才能返回结果。

n=200
x = np.random.randint(0,1,size=n) # initalisation only
while True:
        if x.mean() == 12 and np.median(x) == 6.5:
            break
        else:
            x=np.random.randint(2,40,size=n)

有人可以帮我改进一下这个算法,使得即使n = 5000左右也能快速得到结果吗?


不错的问题,我建议你阅读这个链接(它是一个R函数,但也许可以帮助你)。 - Lauro Bravar
看起来你正在寻找一个随机分布。Numpy几乎拥有所有重要的分布函数 https://docs.scipy.org/doc/numpy/reference/routines.random.html#distributions - Mazdak
看起来这是遗传算法的完美工作。 - Netwave
1
可以在这里看看,获取灵感:https://stackoverflow.com/questions/46565585/random-numbers-in-a-range-around-a-median - nsaura
1
假设 n 必须是偶数?(否则中位数将是一个整数。) - Mark Dickinson
你需要哪种类型的分布? - user2699
5个回答

6

一种获得接近期望结果的方法是生成两个长度为100的随机数范围,满足中位数约束条件并包含所有所需数字范围。然后通过连接这些数组,平均值将约为12但不完全等于12。但由于只涉及平均值,您可以通过调整其中一个数组来生成期望的结果。

In [162]: arr1 = np.random.randint(2, 7, 100)    
In [163]: arr2 = np.random.randint(7, 40, 100)

In [164]: np.mean(np.concatenate((arr1, arr2)))
Out[164]: 12.22

In [166]: np.median(np.concatenate((arr1, arr2)))
Out[166]: 6.5

以下是一个经过向量化和优化的解决方案,相较于使用for循环或Python级别代码的任何其他解决方案都更为高效。该方案通过对随机序列创建进行限制实现了优化:
import numpy as np
import math

def gen_random(): 
    arr1 = np.random.randint(2, 7, 99)
    arr2 = np.random.randint(7, 40, 99)
    mid = [6, 7]
    i = ((np.sum(arr1 + arr2) + 13) - (12 * 200)) / 40
    decm, intg = math.modf(i)
    args = np.argsort(arr2)
    arr2[args[-41:-1]] -= int(intg)
    arr2[args[-1]] -= int(np.round(decm * 40))
    return np.concatenate((arr1, mid, arr2))

演示:

arr = gen_random()
print(np.median(arr))
print(arr.mean())

6.5
12.0

函数背后的逻辑是:
为了得到符合条件的随机数组,我们可以将3个数组 arr1、mid 和 arr2 连接起来。arr1 和 arr2 每个都有99个项,而 mid 包含2个数 6 和 7,这样最终结果就会给出中位数为6.5。现在我们可以创建两个长度为99的随机数组。要使结果具有12的平均值,我们需要找出当前总和与12 * 200之间的差异,并从N个最大的数字中减去结果,在本例中我们可以从arr2中选择它们并使用N=50。
编辑:
如果在结果中有浮点数不是问题,您实际上可以缩短函数如下:
import numpy as np
import math

def gen_random(): 
    arr1 = np.random.randint(2, 7, 99).astype(np.float)
    arr2 = np.random.randint(7, 40, 99).astype(np.float)
    mid = [6, 7]
    i = ((np.sum(arr1 + arr2) + 13) - (12 * 200)) / 40
    args = np.argsort(arr2)
    arr2[args[-40:]] -= i
    return np.concatenate((arr1, mid, arr2))

@Graipher 因为这是Python的random.randint()分布函数的工作方式。它是一种正态分布,以一种使平均值在范围/2左右的方式起作用。其中范围是您传递给random.randint的数字范围。在这种情况下,一个数组给出6/2 = 3,另一个数组给出34/2 = 17,这两者之间的中位数大约为10。但这只是一般推测,由于最终中位数与所有数字相关,因此它会给出比10更多的东西。 - Mazdak
1
谢谢!它运行得很好,而且非常快。我只是在最后添加了random.shuffle(arr)来打乱整个数组。 - MWH
@MWH 如果是这样的话,规范化函数将会更加简化,因为你可以直接从前40个数字中减去“i”。你只需要用“arr2[args[-40:]] -= i”替换掉“i=...”和“return ...”之间的所有行。 - Mazdak
我无法理解。尝试删除所有行之间的内容。出现了args未定义的错误!请问您能否澄清一下您的意思? - MWH
1
这是一个很好的答案。基于此,我会选择两个独立分布,其支持区间在一个有界区间内。按比例缩放它们以获得上述区间。选择形状因子使得均值相加等于所需的均值。 - mikuszefski
显示剩余3条评论

2

在这里,您希望中位数小于平均值。 这意味着均匀分布不适合:您需要许多小值和较少的大值。

具体来说,您想要与大于或等于7的值相同数量的小于或等于6的值。

确保中位数为6.5的简单方法是在[2-6]范围内具有与[7-40]范围内相同数量的值。如果您在两个范围内选择均匀分布,那么您将具有理论平均值13.75,这与所需的12并不远。

权重的轻微变化可以使理论平均值更接近:如果我们使用[7, 8,...,40]范围的random.choices的相对权重[5, 4, 3, 2, 1, 1,… ,1],我们发现该范围的理论平均值为19.98,这足够接近期望的20。

示例代码:

>>> pop1 = list(range(2, 7))
>>> pop2 = list(range(7, 41))
>>> w2 = [ 5, 4, 3, 2 ] + ( [1] * 30)
>>> r1 = random.choices(pop1, k=2500)
>>> r2 = random.choices(pop2, w2, k=2500)
>>> r = r1 + r2
>>> random.shuffle(r)
>>> statistics.mean(r)
12.0358
>>> statistics.median(r)
6.5
>>>

所以现在我们有一个中位数恰好为6.5,均值为12.0358(这个值是随机的,另一个测试将给出稍微不同的值)的5000个值的分布。如果我们想要一个确切的均值为12,我们只需要调整一些值。这里sum(r)是60179,而应该是60000,因此我们必须减少175个值,这些值既不是2(会超出范围),也不是7(会改变中位数)。
最终,一个可能的生成函数可以是:
def gendistrib(n):
    if n % 2 != 0 :
        raise ValueError("gendistrib needs an even parameter")
    n2 = n//2     # n / 2 in Python 2
    pop1 = list(range(2, 7))               # lower range
    pop2 = list(range(7, 41))              # upper range
    w2 = [ 5, 4, 3, 2 ] + ( [1] * 30)      # weights for upper range
    r1 = random.choices(pop1, k=n2)        # lower part of the distrib.
    r2 = random.choices(pop2, w2, k=n2)    # upper part
    r = r1 + r2
    random.shuffle(r)                      # randomize order
    # time to force an exact mean
    tot = sum(r)
    expected = 12 * n
    if tot > expected:                     # too high: decrease some values
        for i, val in enumerate(r):
            if val != 2 and val != 7:
                r[i] = val - 1
                tot -= 1
                if tot == expected:
                    random.shuffle(r)      # shuffle again the decreased values
                    break
    elif tot < expected:                   # too low: increase some values
        for i, val in enumerate(r):
            if val != 6 and val != 40:
                r[i] = val + 1
                tot += 1
                if tot == expected:
                    random.shuffle(r)      # shuffle again the increased values
                    break
    return r

它非常快:我可以在不到0.02秒的时间内使用timeit gendistrib(10000)。但是它不应该用于小分布(少于1000)。


1

好的,您正在查看至少有4个参数的分布 - 其中两个定义范围,另外两个负责所需的平均值和中位数。

我可以想到两种可能性:

  1. 截断正态分布,详情请查看此处。您已经定义了范围,并且需要从平均值和中位数中恢复μ和σ。这将需要解决几个非线性方程,但在Python中相当可行。可以使用https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.truncnorm.html进行抽样。

  2. 4参数Beta分布,请参见此处以获取详细信息。同样,从平均值和中位数中恢复Beta分布中的α和β将需要解决几个非线性方程。知道它们后,可以通过https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.beta.html轻松进行抽样。

更新

这是如何针对从平均值到mu的截断正态分布进行操作的示例:给定均值的截断正态分布

Beta分布没有闭合形式的中位数(除了一些非常简单的边缘情况),因此通常不可能恢复a和b的值。存在一个适用于a,b>1的近似表达式,但在这种情况下不适用(假设表达式,得到的a,b对于mu = 12和median = 6.5 <1)。 - Graipher
@Graipher 虽然这是正确的,但它并不太相关 - 除了几个非线性方程之外,还有中位数的反向表达式。我相信这个函数在Python中是可用的 https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.special.betainc.html - Severin Pappadeux
但是你需要解决不完全beta积分(a,b)==1/2(其中mu = a /(a + b)),以找到a,b的值。虽然第二个比较容易,但第一个不容易,因为您需要不完全beta积分的反函数。不过,您可以通过数值方法来解决它。 - Graipher
@Graipher 当然,我会用数字来完成它。对于截断正态分布也会使用数字方法。 - Severin Pappadeux

0

如果您有许多具有正确中位数和平均值的较小数组,您可以将它们组合起来生成一个更大的数组。

因此...您可以像目前正在做的那样预先生成较小的数组,然后随机组合它们以获得更大的n。当然,这将导致有偏差的随机样本,但听起来您只是想要大致随机的东西。

这里是一个工作(py3)代码,它使用大小为4、6、8、10、...、18的较小样本生成了一个大小为5000的样本,并具有您所需的属性。

请注意,我更改了如何构建较小的随机样本:如果中位数为6.5,则一半的数字必须是<= 6,另一半必须是>= 7,因此我们独立生成这些半数。这会极大地加快速度。

import collections
import numpy as np
import random

rs = collections.defaultdict(list)
for i in range(50):
    n = random.randrange(4, 20, 2)
    while True:
        x=np.append(np.random.randint(2, 7, size=n//2), np.random.randint(7, 41, size=n//2))
        if x.mean() == 12 and np.median(x) == 6.5:
            break
    rs[len(x)].append(x)

def random_range(n):
    if n % 2:
        raise AssertionError("%d must be even" % n)
    r = []
    while n:
        i = random.randrange(4, min(20, n+1), 2)
        # Don't be left with only 2 slots left.
        if n - i == 2: continue
        xs = random.choice(rs[i])
        r.extend(xs)
        n -= i
    random.shuffle(r)
    return r

xs = np.array(random_range(5000))
print([(i, list(xs).count(i)) for i in range(2, 41)])
print(len(xs))
print(xs.mean())
print(np.median(xs))

输出:

[(2, 620), (3, 525), (4, 440), (5, 512), (6, 403), (7, 345), (8, 126), (9, 111), (10, 78), (11, 25), (12, 48), (13, 61), (14, 117), (15, 61), (16, 62), (17, 116), (18, 49), (19, 73), (20, 88), (21, 48), (22, 68), (23, 46), (24, 75), (25, 77), (26, 49), (27, 83), (28, 61), (29, 28), (30, 59), (31, 73), (32, 51), (33, 113), (34, 72), (35, 33), (36, 51), (37, 44), (38, 25), (39, 38), (40, 46)]
5000
12.0
6.5

输出的第一行显示最终数组中有620个2,52个3,440个4等。

0

虽然这篇文章已经有了一个被接受的答案,但我想贡献一个通用的非整数方法。它不需要循环或测试。思路是采用具有紧支撑的PDF。借鉴Kasrâmvd的被接受答案的思路,在左右区间中制作两个分布。选择形状参数使平均值达到给定值。有趣的机会在于,可以创建连续的PDF,即在间隔连接处没有跳跃。

例如,我选择了beta分布。为了在边界处具有有限的非零值,我选择了beta = 1和alpha = 1。

查看PDF的定义和要求的平均值的连续性,得到两个方程:

  • 4.5 / alpha = 33.5 / beta
  • 2 + 6.5 * alpha / ( alpha + 1 ) + 6.5 + 33.5 * 1 / ( 1 + beta ) = 24

这是一个相当容易解决的二次方程。只需使用scipy.stat.beta即可。

from scipy.stats import beta

import matplotlib.pyplot as plt
import numpy as np

x1 = np.linspace(2, 6.5, 200 )
x2 = np.linspace(6.5, 40, 200 )

# i use s and t not alpha and beta
s = 1./737 *(np.sqrt(294118) - 418 )
t = 1./99 *(np.sqrt(294118) - 418 )

data1 = beta.rvs(s, 1, loc=2, scale=4.5, size=20000)
data2 = beta.rvs(1, t, loc=6.5, scale=33.5, size=20000)
data = np.concatenate( ( data1, data2 ) )
print np.mean( data1 ), 2 + 4.5 * s/(1.+s)
print np.mean( data2 ), 6.5 + 33.5/(1.+t) 
print np.mean( data )
print np.median( data )

fig = plt.figure()
ax = fig.add_subplot( 1, 1, 1 )
ax.hist(data1, bins=13, density=True )
ax.hist(data2, bins=67, density=True )
ax.plot( x1, beta.pdf( x1, s, 1, loc=2, scale=4.5 ) )
ax.plot( x2, beta.pdf( x2, 1, t, loc=6.5, scale=33.5 ) )
ax.set_yscale( 'log' )
plt.show()

提供

>> 2.661366939244768 2.6495436216856976
>> 21.297348804473618 21.3504563783143
>> 11.979357871859191
>> 6.5006779033245135

所以结果符合要求,看起来像这样: 输入图像描述


刚刚注意到这也是沿着Severin Pappadeux解决方案的方向。 - mikuszefski

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