Je voudrais 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 est 6,5.

J'ai cherché partout et je n'ai pas trouvé de solution. J'ai essayé le script suivant car il fonctionne pour les petits nombres tels que 20, pour les grands nombres, il faut des âges 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)

Quelqu'un pourrait-il m'aider en améliorant cela pour obtenir un résultat rapide même lorsque n = 5000 environ?

10
MWH 16 avril 2018 à 13:54

5 réponses

Meilleure réponse

Une façon d'obtenir un résultat vraiment proche de ce que vous voulez est de générer deux plages aléatoires distinctes de longueur 100 qui satisfont vos contraintes médianes et incluent toute la plage de nombres désirée. 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 votre résultat attendu en peaufinant 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 qui utilise 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 derrière la fonction:

Pour que nous puissions avoir un tableau aléatoire avec ces critères, nous pouvons concaténer 3 tableaux ensemble arr1, mid et arr2. arr1 et arr2 contiennent chacun 99 éléments et le mid contient 2 éléments 6 et 7 de sorte que le résultat final donne 6,5 comme médiane. Maintenant, nous pouvons créer deux tableaux aléatoires chacun d'une longueur de 99. Tout ce que nous devons faire pour obtenir le résultat pour avoir une moyenne de 12 est de trouver la différence entre la somme actuelle et 12 * 200 et soustraire le résultat de nos N plus grands nombres dans ce cas, nous pouvons les choisir parmi arr2 et utiliser N=50.

Éditer:

Si ce n'est pas un problème d'avoir des nombres flottants dans votre résultat, vous pouvez réellement 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))
5
Kasramvd 17 avril 2018 à 22:10

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 grandes valeurs.

Plus précisément, vous voulez autant de valeurs inférieures ou égales à 6 que le nombre de valeurs supérieures ou égales à 7.

Un moyen simple de garantir que la médiane sera de 6,5 est d'avoir le même nombre de valeurs dans la plage [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 loin des 12 requises.

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

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 qui a 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 nous suffit de modifier certaines valeurs. Ici sum(r) est 60179 alors qu'il devrait être 60000, nous devons donc diminuer 175 valeurs qui n'étaient ni 2 (sortiraient de la plage) ni 7 (changeraient la médiane).

Au final, 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 vraiment rapide: je pourrais chronométrer gendistrib(10000) en moins de 0,02 seconde. Mais il ne doit pas être utilisé pour de petites distributions (moins de 1000)

2
Serge Ballesta 16 avril 2018 à 13:58

Ok, vous regardez la distribution qui a 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 pourrais penser à deux possibilités du haut de ma tête:

  1. Distribution normale tronquée, consultez ici pour plus de détails. Vous avez déjà défini une plage et devez récupérer μ et σ à partir 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 peut être effectué à l'aide de https: //docs.scipy .org / doc / scipy / reference / généré / scipy.stats.truncnorm.html

  2. Distribution bêta à 4 paramètres, voir ici pour plus de détails. Encore une fois, la récupération de α et β dans la distribution bêta à partir de la moyenne et de la médiane nécessitera la résolution de quelques équations non linéaires. La connaissance de leur échantillonnage serait facile via https: // docs .scipy.org / doc / numpy / reference / generated / numpy.random.beta.html

MISE À JOUR

Voici comment vous pouvez le faire pour la normale tronquée allant de la moyenne au mu: Troncated normal avec une moyenne donnée

1
Severin Pappadeux 16 avril 2018 à 16:31

Bien que ce message ait déjà une réponse acceptée, je voudrais apporter une approche générale non entière. Il n'a pas besoin de boucles ou de tests. L'idée est de prendre un PDF avec un support compact. En prenant l'idée de la réponse acceptée de Kasrâmvd, faites deux distributions dans l'intervalle gauche et droit. Choisissez des paramètres de forme tels que la moyenne tombe à la valeur donnée. L'opportunité intéressante ici est que l'on peut créer un PDF continu, c'est-à-dire sans sauts où les intervalles se rejoignent.

À titre d'exemple, j'ai choisi la distribution bêta. Pour avoir des valeurs finies non nulles à la frontière, j'ai choisi beta = 1 pour la gauche et alpha = 1 pour la droite. En regardant la définition du PDF et l'exigence de la moyenne, la continuité donne deux équations:

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

Il s'agit d'une équation quadratique assez facile à résoudre. Le juste en utilisant scipy.stat.beta comme

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

Fournit

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

Donc les résultats sont comme requis et cela ressemble à: entrez la description de l'image ici

0
mikuszefski 27 juin 2019 à 13:27

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

Donc ... vous pouvez pré-générer des tableaux plus petits comme vous le faites actuellement, puis les combiner au hasard pour un n plus grand. Bien sûr, cela se traduira par un échantillon aléatoire biaisé, mais il semble que vous vouliez juste quelque chose qui est approximativement aléatoire.

Voici du code (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 façon dont les petits échantillons aléatoires sont construits: la moitié des nombres doit être <= 6 et la moitié> = 7 si la médiane doit être de 6,5, nous générons donc ces moitiés indépendamment. Cela accélère considérablement 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))

Production:

[(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 montre qu'il y a 620 2, 52 3, 440 4, etc. dans le tableau final.

0
Paul Hankin 16 avril 2018 à 13:57