Ottenere un campione casuale dall'elenco mantenendo l'ordine degli articoli?


84

Ho un elenco ordinato, diciamo: (non è solo numeri, è un elenco di oggetti ordinati con un algoritmo complicato che richiede tempo)

mylist = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,9  , 10 ]

C'è qualche funzione Python che mi darà N degli elementi, ma manterrà l'ordine?

Esempio:

randomList = getRandom(mylist,4)
# randomList = [ 3 , 6 ,7 , 9 ]
randomList = getRandom(mylist,4)
# randomList = [ 1 , 2 , 4 , 8 ]

eccetera...


1
Perché non vuoi random.samplee poi smista?
Daniel Lubarov

È ordinato con un algoritmo non banale ... non sono solo numeri
Yochai Timmer

4
Una modifica molto leggera al commento di Daniel: campiona un intervallo di [0,count), ordina il campione (i numeri nell'intervallo hanno un ordine naturale), quindi estrai i valori in mylistbase agli indici. L'utilizzo zippotrebbe ottenere lo stesso effetto con meccaniche leggermente diverse.

1
ok, posso ottenere una risposta + un esempio in modo da avere qualcosa da accettare? :)
Yochai Timmer

Risposte:


121

Il seguente codice genererà un campione casuale di dimensione 4:

import random

sample_size = 4
sorted_sample = [
    mylist[i] for i in sorted(random.sample(range(len(mylist)), sample_size))
]

(nota: con Python 2, meglio usare xrangeinvece di range)

Spiegazione

random.sample(range(len(mylist)), sample_size)

genera un campione casuale degli indici della lista originale.

Questi indici vengono quindi ordinati per preservare l'ordine degli elementi nell'elenco originale.

Infine, la comprensione della lista estrae gli elementi effettivi dalla lista originale, dati gli indici campionati.


89

Metodo O (N + K * log (K)) semplice da codificare

Prendi un campione casuale senza sostituire gli indici, ordina gli indici e prendili dall'originale.

indices = random.sample(range(len(myList)), K)
[myList[i] for i in sorted(indices)]

O più concisamente:

[x[1] for x in sorted(random.sample(enumerate(myList),K))]

Tempo O (N) ottimizzato, modo O (1) -spazio ausiliario

In alternativa, puoi utilizzare un trucco matematico e scorrere iterativamente myListda sinistra a destra, scegliendo numeri con probabilità che cambiano dinamicamente (N-numbersPicked)/(total-numbersVisited). Il vantaggio di questo approccio è che è un O(N)algoritmo poiché non comporta l'ordinamento!

from __future__ import division

def orderedSampleWithoutReplacement(seq, k):
    if not 0<=k<=len(seq):
        raise ValueError('Required that 0 <= sample_size <= population_size')

    numbersPicked = 0
    for i,number in enumerate(seq):
        prob = (k-numbersPicked)/(len(seq)-i)
        if random.random() < prob:
            yield number
            numbersPicked += 1

Prova di concetto e verifica che le probabilità siano corrette :

Simulato con 1 trilione di campioni pseudocasuali nel corso di 5 ore:

>>> Counter(
        tuple(orderedSampleWithoutReplacement([0,1,2,3], 2))
        for _ in range(10**9)
    )
Counter({
    (0, 3): 166680161, 
    (1, 2): 166672608, 
    (0, 2): 166669915, 
    (2, 3): 166667390, 
    (1, 3): 166660630, 
    (0, 1): 166649296
})

Le probabilità divergono dalle probabilità vere di meno di un fattore 1.0001. L'esecuzione di questo test di nuovo ha prodotto un ordine diverso, il che significa che non è sbilanciato verso un ordine. L'esecuzione del test con un minor numero di campioni [0,1,2,3,4], k=3e ha [0,1,2,3,4,5], k=4avuto risultati simili.

modifica: Non sono sicuro del motivo per cui le persone votano commenti sbagliati o hanno paura di votare a favore ... NO, non c'è niente di sbagliato in questo metodo. =)

(Anche una nota utile dell'utente tegan nei commenti: se questo è python2, vorrai usare xrange, come al solito, se ti interessa davvero lo spazio extra.)

modifica : Dimostrazione: Considerando la distribuzione uniforme (senza sostituzione) di scegliere un sottoinsieme kda una popolazione seqdi dimensioni len(seq), possiamo considerare una partizione in un punto arbitrario iin 'sinistra' (0,1, ..., i-1) e 'right' (i, i + 1, ..., len (seq)). Dato che abbiamo scelto numbersPickeddal sottoinsieme sinistro noto, il resto deve provenire dalla stessa distribuzione uniforme sul sottoinsieme sconosciuto destro, sebbene i parametri siano ora diversi. In particolare, la probabilità che seq[i]contiene un elemento scelto è #remainingToChoose/#remainingToChooseFrom, o(k-numbersPicked)/(len(seq)-i), quindi lo simuliamo e ricorreremo al risultato. (Questo deve terminare poiché se #remainingToChoose == #remainingToChooseFrom, tutte le probabilità rimanenti sono 1.) Questo è simile a un albero di probabilità che sembra essere generato dinamicamente. Fondamentalmente puoi simulare una distribuzione di probabilità uniforme condizionando le scelte precedenti (man mano che aumenti l'albero delle probabilità, scegli la probabilità del ramo corrente in modo che sia aposteriori uguale alle foglie precedenti, cioè condizionata alle scelte precedenti; questa probabilità è uniformemente esattamente N / k).

modifica : Timothy Shields menziona il campionamento del serbatoio , che è la generalizzazione di questo metodo quando len(seq)è sconosciuto (come con un'espressione di generatore). In particolare, quello indicato come "algoritmo R" è lo spazio O (N) e O (1) se eseguito sul posto; implica prendere il primo elemento N e sostituirli lentamente (viene fornito anche un accenno a una dimostrazione induttiva). Ci sono anche utili varianti distribuite e varie varianti del campionamento del giacimento che si trovano sulla pagina di wikipedia.

modifica : ecco un altro modo per codificarlo di seguito in un modo semanticamente più ovvio.

from __future__ import division
import random

def orderedSampleWithoutReplacement(seq, sampleSize):
    totalElems = len(seq)
    if not 0<=sampleSize<=totalElems:
        raise ValueError('Required that 0 <= sample_size <= population_size')

    picksRemaining = sampleSize
    for elemsSeen,element in enumerate(seq):
        elemsRemaining = totalElems - elemsSeen
        prob = picksRemaining/elemsRemaining
        if random.random() < prob:
            yield element
            picksRemaining -= 1

from collections import Counter         
Counter(
    tuple(orderedSampleWithoutReplacement([0,1,2,3], 2))
    for _ in range(10**5)

)


1
@pst: nessuno svantaggio, solo un aumento della velocità di O(N)piuttostoO(N log(N))
ninjagecko

1
Molto carino, mi chiedevo come fare anche questo approccio lineare. Questa formula ha una pagina wikipedia? :)
Jochen Ritzel

2
Sono sorpreso che questa risposta non abbia più voti positivi, in realtà spiega come funziona la soluzione (e fornisce un'altra soluzione!), A differenza della prima risposta che è solo uno snippet di una riga, non dandomi idea del perché o come ha funzionato.
crazy2be

1
Bella soluzione ninjagecko. C'è una bella prova induttiva della tua soluzione se qualcuno è interessato a scriverla.
Neil G,

3
Bella soluzione! Non dimenticare di aggiungere from __future__ import divisionper coloro che eseguono Python 2.
xApple

7

Forse puoi semplicemente generare il campione di indici e quindi raccogliere gli elementi dalla tua lista.

randIndex = random.sample(range(len(mylist)), sample_size)
randIndex.sort()
rand = [mylist[i] for i in randIndex]

4

Apparentemente è random.samplestato introdotto in python 2.3

quindi per la versione inferiore, possiamo usare shuffle (esempio per 4 elementi):

myRange =  range(0,len(mylist)) 
shuffle(myRange)
coupons = [ bestCoupons[i] for i in sorted(myRange[:4]) ]

4
Stai usando Python 2.2 ?! Dovresti aggiornare ... è decisamente obsoleto.
Katriel

1
beh, è ​​quello che abbiamo sui server .. fare un aggiornamento a livello di sistema è troppo Burocrazia
Yochai Timmer

-2

random.sample lo implementa.

>>> random.sample([1, 2, 3, 4, 5],  3)   # Three samples without replacement
[4, 1, 5]

9
Questo non è ordinato.
Astrid
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.