web-dev-qa-db-fra.com

Comment générer des nombres aléatoires pour satisfaire une moyenne et une médiane spécifiques en python?

Je souhaite générer n nombres aléatoires, par exemple n=200, où la plage de valeurs possibles est comprise entre 2 et 40, avec une moyenne de 12 et la médiane de 6,5.

J'ai cherché partout et je n'ai pas trouvé de solution à cela. J'ai essayé le script suivant car cela fonctionne pour de petits nombres tels que 20, pour les gros nombres cela prend des années et le résultat est renvoyé.

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)

Est-ce que quelqu'un pourrait m'aider en améliorant ceci pour obtenir un résultat rapide même lorsque n = 5000 ou plus?

7
MWH

Une façon d'obtenir un résultat très proche de ce que vous souhaitez est de générer deux plages aléatoires distinctes de longueur 100 qui répondent à vos contraintes médianes et incluent toutes les plages de nombres souhaitées. Ensuite, en concaténant les tableaux, la moyenne sera d'environ 12 mais pas tout à fait égale à 12. Mais comme cela signifie simplement que vous avez affaire à vous, vous pouvez simplement générer le résultat attendu en modifiant légèrement l'un de ces tableaux.

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

Voici une solution vectorisée et très optimisée par rapport à toute autre solution utilisant des boucles ou du code de niveau python en contraignant la création de séquence aléatoire:

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

Démo:

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

6.5
12.0

La logique de la fonction:

Pour que nous ayons un tableau aléatoire avec ce critère, nous pouvons concaténer 3 tableaux arr1, midet arr2. arr1 et arr2 contiennent chacun 99 éléments et le midcontient 2 éléments 6 et 7, de sorte que le résultat final donne un résultat égal à 6,5 en tant que médiane. Maintenant, nous créons deux tableaux aléatoires de longueur 99 chacun. Tout ce que nous avons à faire pour obtenir une moyenne de 12 est de trouver la différence entre la somme actuelle et 12 * 200 et de soustraire le résultat de nos N plus grands nombres, ce qui dans ce cas nous pouvons les choisir parmi arr2 et utiliser N=50.

Modifier:

Si ce n'est pas un problème d'avoir des nombres flottants dans votre résultat, vous pouvez raccourcir la fonction comme suit:

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))
4
Kasrâmvd

Ici, vous voulez une valeur médiane inférieure à la valeur moyenne. Cela signifie qu'une distribution uniforme n'est pas appropriée: vous voulez beaucoup de petites valeurs et moins de bonnes.

Spécifiquement, vous voulez autant de valeurs inférieures ou égales à 6 que de nombres de valeurs supérieures ou égales à 7.

Un moyen simple de s'assurer que la médiane sera de 6,5 consiste à avoir le même nombre de valeurs dans l'intervalle [2 - 6] que dans [7 - 40]. Si vous choisissez des distributions uniformes dans les deux plages, vous obtiendrez une moyenne théorique de 13,75, ce qui n’est pas si éloigné de la valeur requise 12.

Une légère variation des poids peut rendre la moyenne théorique encore plus proche: si nous utilisons [5, 4, 3, 2, 1, 1, ..., 1] pour les poids relatifs du random.choices du [7, 8, ..., 40], nous trouvons une moyenne théorique de 19,98 pour cette gamme, ce qui est assez proche de la valeur attendue.

Exemple de code:

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

Nous avons donc maintenant une distribution de 5000 valeurs avec une médiane d’exactement 6,5 et une valeur moyenne de 12,0358 (celle-ci est aléatoire, et un autre test donnera une valeur légèrement différente). Si nous voulons une moyenne exacte de 12, il suffit d’ajuster certaines valeurs. Ici sum(r) est 60179 alors qu'il devrait être 60000, nous devons donc réduire 175 valeurs qui n'étaient ni 2 (sortiraient de la plage) ni 7 (changeraient la médiane).

À la fin, une fonction de générateur possible pourrait être:

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

C'est très rapide: je pourrais timeit gendistrib(10000) en moins de 0,02 seconde. Mais il ne devrait pas être utilisé pour de petites distributions (moins de 1000)

2
Serge Ballesta

Ok, vous regardez la distribution qui ne contient pas moins de 4 paramètres - deux de ceux définissant la plage et deux responsables de la moyenne et de la médiane requises.

Je pouvais penser à deux possibilités du haut de ma tête:

  1. Distribution normale tronquée, regardez ici pour plus de détails. Vous avez déjà défini la plage et devez récupérer les valeurs µ et σ de la moyenne et de la médiane. Cela nécessitera la résolution de quelques équations non linéaires, mais tout à fait faisable en python. L'échantillonnage pourrait être effectué à l'aide de https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.truncnorm.html

  2. Distribution bêta à 4 paramètres, voir ici pour plus de détails. Encore une fois, pour récupérer α et β dans la distribution bêta de la moyenne et de la médiane, il faudra résoudre quelques équations non linéaires. Il serait facile de les connaître, via https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.beta.html

METTRE À JOUR

Voici comment vous pouvez le faire pour une normale tronquée allant de moyenne à mu: Normale tronquée avec une moyenne donnée

1
Severin Pappadeux

Si vous avez un ensemble de tableaux plus petits avec la bonne médiane et la bonne moyenne, vous pouvez les combiner pour produire un tableau plus grand.

Donc ... vous pouvez pré-générer des tableaux plus petits comme vous le faites actuellement, puis les combiner aléatoirement pour des n plus grands. Bien sûr, cela créera un échantillon aléatoire biaisé, mais il semblerait que vous souhaitiez simplement obtenir quelque chose de approximativement aléatoire.

Voici un code de travail (py3) qui génère un échantillon de taille 5000 avec les propriétés souhaitées, qu’il construit à partir d’échantillons plus petits de taille 4, 6, 8, 10, ..., 18.

Notez que j'ai changé la manière dont les échantillons aléatoires plus petits sont construits: la moitié des nombres doit être <= 6 et la moitié> = 7 si la médiane doit être 6,5, nous générons donc ces moitiés de manière indépendante. Cela accélère massivement les choses.

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

Sortie:

[(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

La première ligne de la sortie indique qu'il y a 620 2, 52 3, 440 4, etc. dans le tableau final.

0
Paul Hankin