web-dev-qa-db-fra.com

Sélection aléatoire pondérée à partir du tableau

Je voudrais sélectionner au hasard un élément d'un tableau, mais chaque élément a une probabilité de sélection connue.

Toutes les chances ensemble (dans le tableau) totalisent 1.

Quel algorithme suggéreriez-vous comme le plus rapide et le plus approprié pour des calculs énormes?

Exemple:

id => chance
array[
    0 => 0.8
    1 => 0.2
]

pour ce pseudocode, l'algorithme en question devrait sur plusieurs appels retourner statistiquement quatre éléments sur id 0 pour un élément sur id 1.

67
Mikulas Dite

Calculez la fonction de densité cumulative discrète (CDF) de votre liste - ou, en termes simples, le tableau des sommes cumulées des poids. Générez ensuite un nombre aléatoire compris entre 0 et la somme de tous les poids (peut-être 1 dans votre cas), effectuez une recherche binaire pour trouver ce nombre aléatoire dans votre tableau CDF discret et obtenez la valeur correspondant à cette entrée - ce est votre nombre aléatoire pondéré.

69
Sven Marnach

L'algorithme est simple

Rand_no = Rand(0,1)
for each element in array 
     if(Rand_num < element.probablity)
          select and break
     Rand_num = Rand_num - element.probability
13
Rohit J

J'ai trouvé cet article pour être le plus utile pour bien comprendre ce problème. Cette question de stackoverflow peut également être ce que vous recherchez.


Je pense que la solution optimale consiste à utiliser la méthode Alias ​​(wikipedia) . Il faut O (n) temps pour initialiser, O (1) temps pour faire une sélection, et O (n) mémoire.

Voici l'algorithme pour générer le résultat du laminage d'un dé pondéré n (à partir d'ici, il est trivial de sélectionner un élément à partir d'une longueur - n tableau) comme tiré de cet article . L'auteur suppose que vous avez des fonctions pour lancer un dé juste (floor(random() * n)) et lancer une pièce biaisée (random() < p).

Algorithme: méthode d'alias de Vose

Initialisation:

  1. Créer des tableaux Alias ​​ et Prob , chacun de taille n .
  2. Créez deux listes de travail, Petite et Grande .
  3. Multipliez chaque probabilité par n .
  4. Pour chaque probabilité mise à l'échelle pje:
    1. Si pje <1 , ajoutez i à Petit .
    2. Sinon ( pje ≥ 1 ), ajoutez i à Grand .
  5. Alors que Petit et Grand ne sont pas vides: ( Large peut être vidé en premier)
    1. Supprimez le premier élément de Small ; appelez-le l .
    2. Supprimez le premier élément de Large ; appelez-le g .
    3. Set Prob [l] = pl.
    4. Définissez Alias ​​[l] = g .
    5. Définissez pg : = (pg+ pl) -1 . (Il s'agit d'une option plus stable numériquement.)
    6. Si pg<1 , ajoutez g à Petit .
    7. Sinon ( pg ≥ 1 ), ajoutez g à Grand .
  6. Alors que Large n'est pas vide:
    1. Supprimez le premier élément de Large ; appelez-le g .
    2. Définissez Prob [g] = 1 .
  7. Alors que Petit n'est pas vide: Ceci n'est possible qu'en raison de l'instabilité numérique.
    1. Supprimez le premier élément de Small ; appelez-le l .
    2. Définissez Prob [l] = 1 .

Génération:

  1. Générez un jet de dé équitable à partir d'un dé n ; appeler le côté i .
  2. Lancez une pièce biaisée qui arrive face à face avec probabilité Prob [i] .
  3. Si la pièce sort "têtes", retournez i .
  4. Sinon, retournez Alias ​​[i] .

Cela peut être fait en O(1) temps prévu par échantillon comme suit.

Calculez le CDF F(i) pour chaque élément i comme étant la somme des probabilités inférieure ou égale à i.

Définissez la plage r(i) d'un élément i comme étant l'intervalle [F (i - 1), F (i)].

Pour chaque intervalle [(i - 1)/n, i/n], créez un compartiment composé de la liste des éléments dont la plage chevauche l'intervalle. Cela prend O(n) temps au total pour le tableau complet tant que vous êtes raisonnablement prudent.

Lorsque vous échantillonnez au hasard le tableau, vous calculez simplement dans quel compartiment le nombre aléatoire se trouve et comparez avec chaque élément de la liste jusqu'à ce que vous trouviez l'intervalle qui le contient.

Le coût d'un échantillon est O (la longueur attendue d'une liste choisie au hasard) <= 2.

6
jonderry

Un exemple en Ruby

#each element is associated with its probability
a = {1 => 0.25 ,2 => 0.5 ,3 => 0.2, 4 => 0.05}

#at some point, convert to ccumulative probability
acc = 0
a.each { |e,w| a[e] = acc+=w }

#to select an element, pick a random between 0 and 1 and find the first   
#cummulative probability that's greater than the random number
r = Rand
selected = a.find{ |e,w| w>r }

p selected[0]
6
krusty.ar

Un autre Ruby exemple:

def weighted_Rand(weights = {})
  raise 'Probabilities must sum up to 1' unless weights.values.inject(&:+) == 1.0
  raise 'Probabilities must not be negative' unless weights.values.all? { |p| p >= 0 }
  # Do more sanity checks depending on the amount of trust in the software component using this method
  # E.g. don't allow duplicates, don't allow non-numeric values, etc.

  # Ignore elements with probability 0
  weights = weights.reject { |k, v| v == 0.0 }   # e.g. => {"a"=>0.4, "b"=>0.4, "c"=>0.2}

  # Accumulate probabilities and map them to a value
  u = 0.0
  ranges = weights.map { |v, p| [u += p, v] }   # e.g. => [[0.4, "a"], [0.8, "b"], [1.0, "c"]]

  # Generate a (pseudo-)random floating point number between 0.0(included) and 1.0(excluded)
  u = Rand   # e.g. => 0.4651073966724186

  # Find the first value that has an accumulated probability greater than the random number u
  ranges.find { |p, v| p > u }.last   # e.g. => "b"
end

Comment utiliser:

weights = {'a' => 0.4, 'b' => 0.4, 'c' => 0.2, 'd' => 0.0}

weighted_Rand weights

Quoi attendre:

d = 1000.times.map{ weighted_Rand weights }
d.count('a') # 396
d.count('b') # 406
d.count('c') # 198
5
knugie

Solution Ruby utilisant le gemme de ramassage :

require 'pickup'

chances = {0=>80, 1=>20}
picker = Pickup.new(chances)

Exemple:

5.times.collect {
  picker.pick(5)
}

a donné la sortie:

[[0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 1, 1], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 1]]
3
devstopfix

Ceci est un code PHP que j'ai utilisé dans la production:

/**
 * @return \App\Models\CdnServer
*/
protected function selectWeightedServer(Collection $servers)
{
    if ($servers->count() == 1) {
        return $servers->first();
    }

    $totalWeight = 0;

    foreach ($servers as $server) {
        $totalWeight += $server->getWeight();
    }

    // Select a random server using weighted choice
    $randWeight = mt_Rand(1, $totalWeight);
    $accWeight = 0;

    foreach ($servers as $server) {
        $accWeight += $server->getWeight();

        if ($accWeight >= $randWeight) {
            return $server;
        }
    }
}
2
Gustav.Calder

Si le tableau est petit, je lui donnerais une longueur de, dans ce cas, cinq et attribuerais les valeurs appropriées:

array[
    0 => 0
    1 => 0
    2 => 0
    3 => 0
    4 => 1
]
2
thejh

l'astuce pourrait être d'échantillonner un tableau auxiliaire avec des répétitions d'éléments qui reflètent la probabilité

Compte tenu des éléments associés à leur probabilité, en pourcentage:

h = {1 => 0.5, 2 => 0.3, 3 => 0.05, 4 => 0.05 }

auxiliary_array = h.inject([]){|memo,(k,v)| memo += Array.new((100*v).to_i,k) }   

Ruby-1.9.3-p194 > auxiliary_array 
 => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,                                 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4] 

auxiliary_array.sample

si vous voulez être aussi générique que possible, vous devez calculer le multiplicateur en fonction du nombre maximum de chiffres fractionnaires et l'utiliser à la place de 100:

m = 10**h.values.collect{|e| e.to_s.split(".").last.size }.max
1
masciugo

Je vais améliorer https://stackoverflow.com/users/626341/masciugo réponse.

Fondamentalement, vous créez un grand tableau où le nombre de fois qu'un élément apparaît est proportionnel au poids.

Elle présente certains inconvénients.

  1. Le poids n'est peut-être pas entier. Imaginez que l'élément 1 a une probabilité de pi et l'élément 2 a une probabilité de 1-pi. Comment divisez-vous cela? Ou imaginez s'il existe des centaines de tels éléments.
  2. Le tableau créé peut être très volumineux. Imaginez si le multiplicateur le moins commun est de 1 million, alors nous aurons besoin d'un tableau de 1 million d'éléments dans le tableau que nous voulons choisir.

Pour contrer cela, c'est ce que vous faites.

Créez un tel tableau, mais insérez uniquement un élément au hasard. La probabilité qu'un élément soit inséré est proportionnelle au poids.

Sélectionnez ensuite un élément aléatoire de l'ordinaire.

Donc, s'il y a 3 éléments avec différents poids, vous choisissez simplement un élément dans un tableau de 1 à 3 éléments.

Des problèmes peuvent survenir si l'élément construit est vide. C'est-à-dire qu'il arrive juste qu'aucun élément n'apparaisse dans le tableau parce que leurs dés roulent différemment.

Dans ce cas, je propose que la probabilité qu'un élément soit inséré est p (inséré) = wi/wmax.

De cette façon, un élément, à savoir celui qui a la plus forte probabilité, sera inséré. Les autres éléments seront insérés par la probabilité relative.

Disons que nous avons 2 objets.

l'élément 1 apparaît 0,20% du temps. l'élément 2 affiche 0,40% du temps et présente la probabilité la plus élevée.

Dans le tableau, l'élément 2 apparaîtra tout le temps. L'élément 1 apparaîtra la moitié du temps.

L'élément 2 sera donc appelé 2 fois plus que l'élément 1. Pour la généralité, tous les autres éléments seront appelés proportionnels à leur poids. De plus, la somme de toutes leurs probabilités est 1 car le tableau aura toujours au moins 1 élément.

0
user4951

J'imagine que des nombres supérieurs ou égaux à 0,8 mais inférieurs à 1,0 sélectionnent le troisième élément.

En d'autres termes:

x est un nombre aléatoire compris entre 0 et 1

si 0,0> = x <0,2: élément 1

si 0,2> = x <0,8: élément 2

si 0,8> = x <1,0: élément 3

0
Ryan Rich