Как генерировать случайные числа для удовлетворения определенного среднего и медианы в python?

Я хотел бы генерировать 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 или около того?

4 ответов


один из способов получить результат, действительно близкий к тому, что вы хотите, - это создать два отдельных случайных диапазона длиной 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

следующее является векторизованным и очень оптимизированное решение против любого другого решения, которое использует для циклов или кода уровня 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.

Edit:

если это не проблема, чтобы иметь числа float в вашем результате, вы можете фактически сократить функцию следующим образом:

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))

здесь требуется медианное значение меньше среднего значения. Это означает, что равномерное распределение не подходит: вы хотите много маленьких значений и меньше великих.

в частности, вы хотите, чтобы столько значений меньше или равно 6, сколько число значений больше или равно 7.

простой способ гарантировать, что медиана будет равна 6,5, - это иметь такое же количество значений в диапазоне [ 2 - 6], как и в [ 7 - 40 ]. Если вы выбрали равномерные распределения в обоих диапазонах, у вас будет теоретическое среднее значение 13,75, что не так далеко от требуемого 12.

небольшое изменение веса может сделать в смысле теории еще ближе: если мы используем [ 5, 4, 3, 2, 1, 1, ..., 1 ] для относительных Весов random.choices из [ 7, 8, ..., 40 ] диапазон, мы находим теоретическое среднее значение 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
>>>

Итак, теперь у нас есть распределение значений 5000, которое имеет медиана ровно 6.5 и среднее значение 12.0358 (это одно is случайные, и другой тест даст немного другое значение). Если мы хотим получить точное среднее 12, нам просто нужно изменить некоторые значения. Вот!--3--> 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

Это очень быстро: я мог timeit gendistrib(10000) менее 0,02 секунды. Но он не должен использоваться для небольших распределений (менее 1000)


хорошо, вы смотрите на распределение, которое имеет не менее 4 параметров-два из тех, которые определяют диапазон и два отвечают за требуемое среднее и медиану.

Я мог думать о двух возможностях из верхней части моей головы:

  1. усеченное нормальное распределение, смотри здесь для сведения. Вы уже определили диапазон и должны восстановить μ и σ из среднего и медианы. Это потребует решения пары нелинейных уравнений, но довольно выполнимо в Python. Забор смог быть сделан используя https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.truncnorm.html

  2. 4-параметры бета-распределения, см. здесь для сведения. Опять же, восстановление α и β в бета-распределении от среднего и медианы потребует решения пары нелинейных уравнений. Зная их выборки будет легко через https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.beta.html

обновление

вот как вы могли бы сделать это для усеченного нормального перехода от среднего к mu:усеченный нормальный с заданным средним


Если у вас есть куча меньших массивов с правой медианой и средним значением, вы можете объединить их для создания большего массива.

Так... вы можете предварительно генерировать меньшие массивы, как вы сейчас делаете, а затем комбинировать их случайным образом для большего n. Конечно, это приведет к предвзятой случайной выборке, но похоже, что вы просто хотите что-то приблизительно случайное.

вот рабочий (py3) код, который генерирует образец размера 5000 с вашими желаемыми свойствами, что он строит от более малых образцов размера 4, 6, 8, 10 ..., 18.

обратите внимание, что я изменил способ построения меньших случайных выборок: половина чисел должна быть = 7, Если медиана должна быть 6.5, поэтому мы генерируем эти половины независимо. Это ускоряет вещи массово.

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 и т. д. в финальном массиве.