Weighted random selection from array

后端 未结 13 1770
醉酒成梦
醉酒成梦 2020-11-28 03:13

I would like to randomly select one element from an array, but each element has a known probability of selection.

All chances together (within the array) sums to 1.<

相关标签:
13条回答
  • 2020-11-28 03:25

    I am going to improve on https://stackoverflow.com/users/626341/masciugo answer.

    Basically you make one big array where the number of times an element shows up is proportional to the weight.

    It has some drawbacks.

    1. The weight might not be integer. Imagine element 1 has probability of pi and element 2 has probability of 1-pi. How do you divide that? Or imagine if there are hundreds of such elements.
    2. The array created can be very big. Imagine if least common multiplier is 1 million, then we will need an array of 1 million element in the array we want to pick.

    To counter that, this is what you do.

    Create such array, but only insert an element randomly. The probability that an element is inserted is proportional the the weight.

    Then select random element from usual.

    So if there are 3 elements with various weight, you simply pick an element from an array of 1-3 elements.

    Problems may arise if the constructed element is empty. That is it just happens that no elements show up in the array because their dice roll differently.

    In which case, I propose that the probability an element is inserted is p(inserted)=wi/wmax.

    That way, one element, namely the one that has the highest probability, will be inserted. The other elements will be inserted by the relative probability.

    Say we have 2 objects.

    element 1 shows up .20% of the time. element 2 shows up .40% of the time and has the highest probability.

    In thearray, element 2 will show up all the time. Element 1 will show up half the time.

    So element 2 will be called 2 times as many as element 1. For generality all other elements will be called proportional to their weight. Also the sum of all their probability are 1 because the array will always have at least 1 element.

    0 讨论(0)
  • 2020-11-28 03:30

    The algorithm is straight forward

    rand_no = rand(0,1)
    for each element in array 
         if(rand_num < element.probablity)
              select and break
         rand_num = rand_num - element.probability
    
    0 讨论(0)
  • 2020-11-28 03:31

    "Wheel of Fortune" O(n), use for small arrays only:

    function pickRandomWeighted(array, weights) {
        var sum = 0;
        for (var i=0; i<weights.length; i++) sum += weights[i];
        for (var i=0, pick=Math.random()*sum; i<weights.length; i++, pick-=weights[i])
            if (pick-weights[i]<0) return array[i];
    }
    
    0 讨论(0)
  • 2020-11-28 03:35

    Compute the discrete cumulative density function (CDF) of your list -- or in simple terms the array of cumulative sums of the weights. Then generate a random number in the range between 0 and the sum of all weights (might be 1 in your case), do a binary search to find this random number in your discrete CDF array and get the value corresponding to this entry -- this is your weighted random number.

    0 讨论(0)
  • 2020-11-28 03:35

    the trick could be to sample an auxiliary array with elements repetitions which reflect the probability

    Given the elements associated with their probability, as percentage:

    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
    

    if you want to be as generic as possible, you need to calculate the multiplier based on the max number of fractional digits, and use it in the place of 100:

    m = 10**h.values.collect{|e| e.to_s.split(".").last.size }.max
    
    0 讨论(0)
  • 2020-11-28 03:36

    Another Ruby example:

    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
    

    How to use:

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

    What to expect roughly:

    sample = 1000.times.map{ weighted_rand weights }
    sample.count('a') # 396
    sample.count('b') # 406
    sample.count('c') # 198
    sample.count('d') # 0
    
    0 讨论(0)
提交回复
热议问题