2010-11-06 15 views
26
import random 
pos = ["A", "B", "C"] 
x = random.choice["A", "B", "C"] 

Este código me da "A", "B" o "C" con la misma probabilidad. ¿Hay una buena manera de expresarlo cuando quiere "A" con 30%, "B" con 40% y "C" con 30% de probabilidad?Forma pitónica para seleccionar elementos de lista con diferente probabilidad

+0

La búsqueda encontró varias preguntas similares/idénticas [aquí] (http://stackoverflow.com/questions/526255/probability-distribution-in-python) y [aquí] (http://stackoverflow.com/questions/1056151/random-python-dictionary-key-weighted-by-values) – snapshoe

+4

@ ma3: Uno de los puntos débiles de este sitio es que, como las viejas preguntas tienden a ignorarse, no existe un mecanismo o motivación para mejorarlas. Creo que mi respuesta es significativamente mejor que al menos las respuestas más altas a esas preguntas, no he leído las más bajas, pero nadie lo vería si las publicara en esas. –

Respuesta

29

Los pesos definen una función de distribución de probabilidad (pdf). Los números aleatorios a partir de cualquiera de tales pdf pueden ser generados por applying its associated inverse cumulative distribution function a números aleatorios uniformes entre 0 y 1.

Ver también este SO explanation, o, como se explica por Wikipedia:

If Y has a U[0,1] distribution then F⁻¹(Y) is distributed as F. This is used in random number generation using the inverse transform sampling-method.

import random 
import bisect 
import collections 

def cdf(weights): 
    total = sum(weights) 
    result = [] 
    cumsum = 0 
    for w in weights: 
     cumsum += w 
     result.append(cumsum/total) 
    return result 

def choice(population, weights): 
    assert len(population) == len(weights) 
    cdf_vals = cdf(weights) 
    x = random.random() 
    idx = bisect.bisect(cdf_vals, x) 
    return population[idx] 

weights=[0.3, 0.4, 0.3] 
population = 'ABC' 
counts = collections.defaultdict(int) 
for i in range(10000): 
    counts[choice(population, weights)] += 1 
print(counts) 

# % test.py 
# defaultdict(<type 'int'>, {'A': 3066, 'C': 2964, 'B': 3970}) 
enter code here 

La función choice anteriormente usa bisect.bisect, por lo que la selección de una variable aleatoria ponderada se realiza en O(log n) donde n es la longitud de weights.


Nota que a partir de la versión 1.7.0, NumPy tiene una Cythonized np.random.choice function. Por ejemplo, esto genera 1000 muestras de la población [0,1,2,3] con pesos [0.1, 0.2, 0.3, 0.4]:

import numpy as np 
np.random.choice(4, 1000, p=[0.1, 0.2, 0.3, 0.4]) 

np.random.choice también tiene un parámetro replace para el muestreo con o sin sustitución.


Un teóricamente mejor algoritmo es el Alias Method. Construye una tabla que requiere O(n) tiempo, pero después de eso, las muestras se pueden dibujar en O(1) vez. Entonces, si necesita dibujar muchas muestras, en teoría, el Método Alias ​​puede ser más rápido. Hay una implementación de Python del Método Walker Alias ​​here y un numpy version here.

21

No ... tanto ...

pos = ['A'] * 3 + ['B'] * 4 + ['C'] * 3 
print random.choice(pos) 

o

pos = {'A': 3, 'B': 4, 'C': 3} 
print random.choice([x for x in pos for y in range(pos[x])]) 
+4

Si las relaciones provienen de la entrada del usuario, esto es potencialmente peligroso, ej. 99.999999%/0.000001%. –

+0

¿Es posible de alguna manera incorporar el diccionario en 'random.choice()'? –

+0

@SomeGuy: 'random.choice()' toma una secuencia. –

9

Aquí es una clase para exponer un montón de elementos con probabilidades relativas, sin tener que ampliar la lista:

import bisect 
class WeightedTuple(object): 
    """ 
    >>> p = WeightedTuple({'A': 2, 'B': 1, 'C': 3}) 
    >>> len(p) 
    6 
    >>> p[0], p[1], p[2], p[3], p[4], p[5] 
    ('A', 'A', 'B', 'C', 'C', 'C') 
    >>> p[-1], p[-2], p[-3], p[-4], p[-5], p[-6] 
    ('C', 'C', 'C', 'B', 'A', 'A') 
    >>> p[6] 
    Traceback (most recent call last): 
    ... 
    IndexError 
    >>> p[-7] 
    Traceback (most recent call last): 
    ... 
    IndexError 
    """ 
    def __init__(self, items): 
     self.indexes = [] 
     self.items = [] 
     next_index = 0 
     for key in sorted(items.keys()): 
      val = items[key] 
      self.indexes.append(next_index) 
      self.items.append(key) 
      next_index += val 

     self.len = next_index 

    def __getitem__(self, n): 
     if n < 0: 
      n = self.len + n 
     if n < 0 or n >= self.len: 
      raise IndexError 

     idx = bisect.bisect_right(self.indexes, n) 
     return self.items[idx-1] 

    def __len__(self): 
     return self.len 

Ahora, acaba de decir:

data = WeightedTuple({'A': 30, 'B': 40, 'C': 30}) 
random.choice(data) 
+0

Tenga en cuenta que solo ordené las claves antes de insertar (en lugar de simplemente usar 'items.iteritems') para que tenga una salida determinística, lo que hace que los doctests sean más simples –

+0

Tenga en cuenta que he evitado el punto flotante: si algo se puede hacer claramente con enteros, evita cualquier cuestión de error de redondeo que afecte a la salida, y es mucho más fácil para el doctest ser exhaustivo. Además, esto no importa lo que La suma de los valores es, por ejemplo, que se normaliza implícitamente. Por ejemplo, si está eligiendo de una lista de espejos ponderados y deshabilita uno de ellos, no tiene que ajuste todo el resto para que los pesos se sumen a 1. –

+2

Tenga en cuenta que 'sorted (dict.keys())' es un poco redundante. 'sorted (d)' hace lo mismo con una lista menos. –

0

Prueba esto:

import random 
from decimal import Decimal 

pos = {'A': Decimal("0.3"), 'B': Decimal("0.4"), 'C': Decimal("0.3")} 
choice = random.random() 
F_x = 0 
for k, p in pos.iteritems(): 
    F_x += p 
    if choice <= F_x: 
     x = k 
     break 
+0

'TypeError: No se puede convertir el flotante en Decimal. Primero convierta el flotador en una cadena' –

4

También puede hacer uso de este formulario, que no crea una lista arbitrariamente grande (y puede trabajar con cualquiera de las probabilidades integrales o decimales):

pos = [("A", 30), ("B", 40), ("C", 30)] 


from random import uniform 
def w_choice(seq): 
    total_prob = sum(item[1] for item in seq) 
    chosen = random.uniform(0, total_prob) 
    cumulative = 0 
    for item, probality in seq: 
     cumulative += probality 
     if cumulative > chosen: 
      return item 
5

Hay Aquí se ofrecen algunas buenas soluciones, pero le sugiero que consulte Eli Bendersky's thorough discussion de esta edición, que compara varios algoritmos para lograr esto (con implementaciones en Python) antes de elegir uno.

Cuestiones relacionadas