Как генерировать случайные числа для удовлетворения определенного среднего и медианы в 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 параметров-два из тех, которые определяют диапазон и два отвечают за требуемое среднее и медиану.
Я мог думать о двух возможностях из верхней части моей головы:
усеченное нормальное распределение, смотри здесь для сведения. Вы уже определили диапазон и должны восстановить μ и σ из среднего и медианы. Это потребует решения пары нелинейных уравнений, но довольно выполнимо в Python. Забор смог быть сделан используя https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.truncnorm.html
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 и т. д. в финальном массиве.