web-dev-qa-db-fra.com

Choisissez N éléments au hasard dans une séquence de longueur inconnue

J'essaie d'écrire un algorithme qui sélectionnerait aléatoirement N éléments distincts d'une séquence, sans connaître la taille de la séquence à l'avance, et où il est coûteux de parcourir la séquence plusieurs fois. Par exemple, les éléments de la séquence peuvent être les lignes d’un fichier volumineux.

J'ai trouvé une solution lorsque N = 1 (c'est-à-dire lorsque vous essayez de choisir exactement un élément au hasard dans une séquence énorme):

import random
items = range(1, 10) # Imagine this is a huge sequence of unknown length
count = 1
selected = None
for item in items:
    if random.random() * count < 1:
        selected = item
    count += 1

Mais comment puis-je obtenir la même chose pour d'autres valeurs de N (disons, N = 3)?

36
akonsu

Utiliser échantillonnage de réservoir . C'est un algorithme très simple qui fonctionne pour tout N.

Ici est une implémentation de Python, et ici est une autre.

41
NPE

Si votre séquence est suffisamment courte pour que la lecture en mémoire et le tri aléatoire soient acceptables, une approche simple consisterait simplement à utiliser random.shuffle :

import random
arr=[1,2,3,4]

# In-place shuffle
random.shuffle(arr)

# Take the first 2 elements of the now randomized array
print arr[0:2]
[1, 3]

Selon le type de votre séquence, vous devrez peut-être la convertir en liste en appelant list(your_sequence), mais cela fonctionnera quels que soient les types d'objets de votre séquence.

Naturellement, si vous ne pouvez pas adapter votre séquence à la mémoire ou si les besoins en mémoire ou en CPU de cette approche sont trop élevés pour vous, vous devrez utiliser une solution différente.

65
Carl Bellingan

Le plus simple que j'ai trouvé est this answer in SO:

import random

my_list = [1, 2, 3, 4, 5]
num_selections = 2

new_list = random.sample(my_list, num_selections)

# To preserve the order of the list, you could do:
randIndex = random.sample(range(len(my_list)), n_selections)
randIndex.sort()
new_list = [my_list[i] for i in randIndex]
20
Solomon Vimal

Si vous avez la version 3.6 ou ultérieure de Python, vous pouvez choisir entre 

from random import choices

items = range(1, 10)
new_items = choices(items, k = 3)

print(new_items) 
[6, 3, 1]
13
Christof Henkel

@NPE est correct, mais les implémentations qui sont liées sont sous-optimales et peu "pythoniques". Voici une meilleure implémentation:

def sample(iterator, k):
    """
    Samples k elements from an iterable object.

    :param iterator: an object that is iterable
    :param k: the number of items to sample
    """
    # fill the reservoir to start
    result = [next(iterator) for _ in range(k)]

    n = k - 1
    for item in iterator:
        n += 1
        s = random.randint(0, n)
        if s < k:
            result[s] = item

    return result

Edit Comme @ panda-34 a fait remarquer que la version originale était défectueuse, mais pas parce que j'utilisais randint vs randrange. Le problème est que ma valeur initiale pour n ne tenait pas compte du fait que randint est inclusif aux deux extrémités de la plage. Prendre cela en compte corrige le problème. (Remarque: vous pouvez également utiliser randrange car il est inclus pour la valeur minimale et exclusif pour la valeur maximale.)

4
JesseBuesking

La suite vous donnera N éléments aléatoires d'un tableau X

import random
list(map(lambda _: random.choice(X), range(N)))
4
Shubham Chaudhary

Cela devrait suffire d'accepter ou de rejeter chaque nouvel élément une seule fois et, si vous l'acceptez, de jeter un ancien article choisi au hasard.

Supposons que vous avez sélectionné N éléments de K au hasard et que vous voyez un (K + 1) ème élément. Acceptez-le avec une probabilité N/(K + 1) et ses probabilités sont acceptables. Les items actuels sont entrés avec probabilité N/K, et sont jetés avec probabilité (N/(K + 1)) (1/N) = 1/(K + 1) afin de survivre avec probabilité (N/K ) (K/(K + 1)) = N/(K + 1), donc leurs probabilités sont bien aussi.

Et oui, je vois que quelqu'un vous a demandé de faire un échantillonnage de réservoir - ceci explique en partie comment cela fonctionne.

3
mcdowella

Comme mentionné, Aix a mentionné que les travaux d’échantillonnage dans les réservoirs étaient efficaces. Une autre option consiste à générer un nombre aléatoire pour chaque nombre affiché et à sélectionner les k premiers chiffres. 

Pour le faire de manière itérative, maintenez un tas de k paires (nombre aléatoire) et chaque fois que vous voyez un nouveau nombre insérer dans le tas s'il est supérieur à la plus petite valeur du tas.

2
ElKamina

C'était ma réponse à une question en double (fermée avant que je puisse poster) qui était quelque peu liée ("générer des nombres aléatoires sans aucun doublon"). Comme c'est une approche différente des autres réponses, je vais le laisser ici au cas où cela fournirait des informations supplémentaires.

from random import randint

random_nums = []
N = # whatever number of random numbers you want
r = # lower bound of number range
R = # upper bound of number range

x = 0

while x < N:
    random_num = randint(r, R) # inclusive range
    if random_num in random_nums:
        continue
    else:
        random_nums.append(random_num)
        x += 1

La raison de la boucle while sur la boucle for est qu’elle permet une implémentation plus aisée de la génération aléatoire (par exemple, si vous obtenez 3 doublons, vous n’obtiendrez pas de nombres N-3).

0
tooty44