it-roy-ru.com

выбор на основе процентного веса

У меня есть набор значений и связанный процент для каждого:

а: шанс 70%
б: 20% шанс
c: 10% шанс

Я хочу выбрать значение (a, b, c) в зависимости от процентной вероятности.

как мне подойти к этому?


Моя попытка пока выглядит так:

r = random.random()
if r <= .7:
    return a
Elif r <= .9:
    return b
else: 
    return c

Я застрял, придумывая алгоритм, чтобы справиться с этим. Как мне подходить к этому, чтобы он мог обрабатывать большие наборы значений, не просто связывая воедино потоки if-else. 


(Любое объяснение или ответы в псевдокоде - это хорошо. Реализация на python или C # будет особенно полезной)

26
Corey Goldberg

Вот полное решение в C #:

public class ProportionValue<T>
{
    public double Proportion { get; set; }
    public T Value { get; set; }
}

public static class ProportionValue
{
    public static ProportionValue<T> Create<T>(double proportion, T value)
    {
        return new ProportionValue<T> { Proportion = proportion, Value = value };
    }

    static Random random = new Random();
    public static T ChooseByRandom<T>(
        this IEnumerable<ProportionValue<T>> collection)
    {
        var rnd = random.NextDouble();
        foreach (var item in collection)
        {
            if (rnd < item.Proportion)
                return item.Value;
            rnd -= item.Proportion;
        }
        throw new InvalidOperationException(
            "The proportions in the collection do not add up to 1.");
    }
}

Использование:

var list = new[] {
    ProportionValue.Create(0.7, "a"),
    ProportionValue.Create(0.2, "b"),
    ProportionValue.Create(0.1, "c")
};

// Outputs "a" with probability 0.7, etc.
Console.WriteLine(list.ChooseByRandom());
36
Timwi

Для Python:

>>> import random
>>> dst = 70, 20, 10
>>> vls = 'a', 'b', 'c'
>>> picks = [v for v, d in Zip(vls, dst) for _ in range(d)]
>>> for _ in range(12): print random.choice(picks),
... 
a c c b a a a a a a a a
>>> for _ in range(12): print random.choice(picks),
... 
a c a c a b b b a a a a
>>> for _ in range(12): print random.choice(picks),
... 
a a a a c c a c a a c a
>>> 

Общая идея: составить список, в котором каждый элемент повторяется несколько раз пропорционально вероятности, которую он должен иметь; используйте random.choice, чтобы выбрать один случайный (равномерно), это будет соответствовать вашему требуемому распределению вероятности. Может быть немного расточительно, если ваши вероятности выражены особым образом (например, 70, 20, 10 составляет список из 100 элементов, где 7, 2, 1 составляет список всего из 10 элементов с точно таким же поведением), но вы можете разделить все числа в список вероятностей по величине их общего фактора, если вы думаете, что это может иметь большое значение в вашем конкретном сценарии приложения.

Помимо проблем с потреблением памяти, это должно быть самым быстрым решением - всего одно генерирование случайного числа на требуемый выходной результат и максимально быстрый поиск из этого случайного числа, без сравнений и т.д. Если ваши вероятные вероятности очень странные (например, числа с плавающей запятой, которые должны быть сопоставлены со многими, многими значащими цифрами), другие подходы могут быть предпочтительнее ;-).

9
Alex Martelli

Кнут ссылается на метод псевдонимов Уокера. Поиск по этому, я нахожу http://code.activestate.com/recipes/576564-walkers-alias-method-for-random-objects-with-diffe/ и http: //prxq.wordpress .com/2006/04/17/метод псевдонимов/ . Это дает точные вероятности, требуемые в постоянном времени для числа, сгенерированного с линейным временем для настройки (любопытно, что n log n время для настройки, если вы используете точно метод, описанный Кнутом, который делает подготовительную сортировку, которую вы можете избежать).

8
mcdowella

Возьмите список и найдите совокупное значение весов: 70, 70 + 20, 70 + 20 + 10. Выберите случайное число больше или равно нулю и меньше, чем общее. Выполните итерацию по элементам и верните первое значение, для которого накопленная сумма весов больше этого случайного числа:

def select( values ):
    variate = random.random() * sum( values.values() )
    cumulative = 0.0
    for item, weight in values.items():
        cumulative += weight
        if variate < cumulative:
            return item
    return item # Shouldn't get here, but just in case of rounding...

print select( { "a": 70, "b": 20, "c": 10 } )

Это решение, в том виде, в котором оно реализовано, должно также иметь возможность обрабатывать дробные веса и веса, которые в сумме дают любое число, при условии, что все они неотрицательны.

6
Boojum
  1. Пусть T = сумма весов всех предметов
  2. Пусть R = случайное число между 0 и T
  3. Выполните итерацию списка элементов, вычитая вес каждого элемента из R, и верните элемент, в результате которого результат станет <= 0.
3
ChrisH
def weighted_choice(probabilities):
    random_position = random.random() * sum(probabilities)
    current_position = 0.0
    for i, p in enumerate(probabilities):
        current_position += p
        if random_position < current_position:
            return i
    return None

Поскольку random.random всегда будет возвращать <1.0, окончательная return никогда не будет достигнута.

3
Mark Ransom
import random

def selector(weights):
    i=random.random()*sum(x for x,y in weights)
    for w,v in weights:
        if w>=i:
            break
        i-=w
    return v

weights = ((70,'a'),(20,'b'),(10,'c'))
print [selector(weights) for x in range(10)] 

это работает одинаково хорошо для дробных весов

weights = ((0.7,'a'),(0.2,'b'),(0.1,'c'))
print [selector(weights) for x in range(10)] 

Если у вас есть lot весов, вы можете использовать bisect, чтобы уменьшить количество необходимых итераций

import random
import bisect

def make_acc_weights(weights):
    acc=0
    acc_weights = []
    for w,v in weights:
        acc+=w
        acc_weights.append((acc,v))
    return acc_weights

def selector(acc_weights):
    i=random.random()*sum(x for x,y in weights)
    return weights[bisect.bisect(acc_weights, (i,))][1]

weights = ((70,'a'),(20,'b'),(10,'c'))
acc_weights = make_acc_weights(weights)    
print [selector(acc_weights) for x in range(100)]

Также отлично работает для дробных весов

weights = ((0.7,'a'),(0.2,'b'),(0.1,'c'))
acc_weights = make_acc_weights(weights)    
print [selector(acc_weights) for x in range(100)]
2
John La Rooy

сегодня, обновление документа на python приведем пример для создания random.choice () со взвешенными вероятностями:

Если веса представляют собой небольшие целочисленные отношения, простой метод состоит в создании выборочной совокупности с повторениями:

>>> weighted_choices = [('Red', 3), ('Blue', 2), ('Yellow', 1), ('Green', 4)]
>>> population = [val for val, cnt in weighted_choices for i in range(cnt)]
>>> random.choice(population)
'Green'

Более общий подход состоит в том, чтобы расположить веса в кумулятивном распределении с помощью itertools.accumulate (), а затем найти случайное значение с помощью bisect.bisect ():

>>> choices, weights = Zip(*weighted_choices)
>>> cumdist = list(itertools.accumulate(weights))
>>> x = random.random() * cumdist[-1]
>>> choices[bisect.bisect(cumdist, x)]
'Blue'

одно замечание: itertools.accumulate () требует Python 3.2 или определяет его с помощью Эквивалента.

2
sunqiang

Я думаю, что у вас может быть массив небольших объектов (я реализовал на Java, хотя я немного знаю C #, но, боюсь, могу написать неправильный код), поэтому вам, возможно, придется портировать его самостоятельно. Код на C # будет намного меньше с struct, var, но я надеюсь, что вы поняли идею

class PercentString {
  double percent;
  String value;
  // Constructor for 2 values
}

ArrayList<PercentString> list = new ArrayList<PercentString();
list.add(new PercentString(70, "a");
list.add(new PercentString(20, "b");
list.add(new PercentString(10, "c");

double percent = 0;
for (int i = 0; i < list.size(); i++) {
  PercentString p = list.get(i);
  percent += p.percent;
  if (random < percent) {
    return p.value;
  }
}
1
vodkhang

У меня есть собственное решение для этого:

public class Randomizator3000 
{    
public class Item<T>
{
    public T value;
    public float weight;

    public static float GetTotalWeight<T>(Item<T>[] p_itens)
    {
        float __toReturn = 0;
        foreach(var item in p_itens)
        {
            __toReturn += item.weight;
        }

        return __toReturn;
    }
}

private static System.Random _randHolder;
private static System.Random _random
{
    get 
    {
        if(_randHolder == null)
            _randHolder = new System.Random();

        return _randHolder;
    }
}

public static T PickOne<T>(Item<T>[] p_itens)
{   
    if(p_itens == null || p_itens.Length == 0)
    {
        return default(T);
    }

    float __randomizedValue = (float)_random.NextDouble() * (Item<T>.GetTotalWeight(p_itens));
    float __adding = 0;
    for(int i = 0; i < p_itens.Length; i ++)
    {
        float __cacheValue = p_itens[i].weight + __adding;
        if(__randomizedValue <= __cacheValue)
        {
            return p_itens[i].value;
        }

        __adding = __cacheValue;
    }

    return p_itens[p_itens.Length - 1].value;

}
}

И использовать это должно быть что-то вроде этого (это в Unity3d)

using UnityEngine;
using System.Collections;

public class teste : MonoBehaviour 
{
Randomizator3000.Item<string>[] lista;

void Start()
{
    lista = new Randomizator3000.Item<string>[10];
    lista[0] = new Randomizator3000.Item<string>();
    lista[0].weight = 10;
    lista[0].value = "a";

    lista[1] = new Randomizator3000.Item<string>();
    lista[1].weight = 10;
    lista[1].value = "b";

    lista[2] = new Randomizator3000.Item<string>();
    lista[2].weight = 10;
    lista[2].value = "c";

    lista[3] = new Randomizator3000.Item<string>();
    lista[3].weight = 10;
    lista[3].value = "d";

    lista[4] = new Randomizator3000.Item<string>();
    lista[4].weight = 10;
    lista[4].value = "e";

    lista[5] = new Randomizator3000.Item<string>();
    lista[5].weight = 10;
    lista[5].value = "f";

    lista[6] = new Randomizator3000.Item<string>();
    lista[6].weight = 10;
    lista[6].value = "g";

    lista[7] = new Randomizator3000.Item<string>();
    lista[7].weight = 10;
    lista[7].value = "h";

    lista[8] = new Randomizator3000.Item<string>();
    lista[8].weight = 10;
    lista[8].value = "i";

    lista[9] = new Randomizator3000.Item<string>();
    lista[9].weight = 10;
    lista[9].value = "j";
}


void Update () 
{
    Debug.Log(Randomizator3000.PickOne<string>(lista));
}
}

В этом примере каждое значение имеет 10% шанс, что будет отображаться как debug = 3

1
Ivan Cavalheiro

Если вы действительно разбираетесь в скорости и хотите быстро генерировать случайные значения, алгоритм mcdowella Уокера, упомянутый в https://stackoverflow.com/a/3655773/1212517 , является в значительной степени наилучшим способом (O ( 1) время для random () и O(N) время для preprocess ()). 

Для тех, кто заинтересован, вот моя собственная PHP реализация алгоритма:

/**
 * Pre-process the samples (Walker's alias method).
 * @param array key represents the sample, value is the weight
 */
protected function preprocess($weights){

    $N = count($weights);
    $sum = array_sum($weights);
    $avg = $sum / (double)$N;

    //divide the array of weights to values smaller and geq than sum/N 
    $smaller = array_filter($weights, function($itm) use ($avg){ return $avg > $itm;}); $sN = count($smaller); 
    $greater_eq = array_filter($weights, function($itm) use ($avg){ return $avg <= $itm;}); $gN = count($greater_eq);

    $bin = array(); //bins

    //we want to fill N bins
    for($i = 0;$i<$N;$i++){
        //At first, decide for a first value in this bin
        //if there are small intervals left, we choose one
        if($sN > 0){  
            $choice1 = each($smaller); 
            unset($smaller[$choice1['key']]);
            $sN--;
        } else{  //otherwise, we split a large interval
            $choice1 = each($greater_eq); 
            unset($greater_eq[$choice1['key']]);
        }

        //splitting happens here - the unused part of interval is thrown back to the array
        if($choice1['value'] >= $avg){
            if($choice1['value'] - $avg >= $avg){
                $greater_eq[$choice1['key']] = $choice1['value'] - $avg;
            }else if($choice1['value'] - $avg > 0){
                $smaller[$choice1['key']] = $choice1['value'] - $avg;
                $sN++;
            }
            //this bin comprises of only one value
            $bin[] = array(1=>$choice1['key'], 2=>null, 'p1'=>1, 'p2'=>0);
        }else{
            //make the second choice for the current bin
            $choice2 = each($greater_eq);
            unset($greater_eq[$choice2['key']]);

            //splitting on the second interval
            if($choice2['value'] - $avg + $choice1['value'] >= $avg){
                $greater_eq[$choice2['key']] = $choice2['value'] - $avg + $choice1['value'];
            }else{
                $smaller[$choice2['key']] = $choice2['value'] - $avg + $choice1['value'];
                $sN++;
            }

            //this bin comprises of two values
            $choice2['value'] = $avg - $choice1['value'];
            $bin[] = array(1=>$choice1['key'], 2=>$choice2['key'],
                           'p1'=>$choice1['value'] / $avg, 
                           'p2'=>$choice2['value'] / $avg);
        }
    }

    $this->bins = $bin;
}

/**
 * Choose a random sample according to the weights.
 */
public function random(){
    $bin = $this->bins[array_Rand($this->bins)];
    $randValue = (lcg_value() < $bin['p1'])?$bin[1]:$bin[2];        
}
0
user1212517

Вот моя версия, которая может применяться к любому IList и нормализовать вес. Он основан на решении Тимви: выбор на основе процентного веса

/// <summary>
/// return a random element of the list or default if list is empty
/// </summary>
/// <param name="e"></param>
/// <param name="weightSelector">
/// return chances to be picked for the element. A weigh of 0 or less means 0 chance to be picked.
/// If all elements have weight of 0 or less they all have equal chances to be picked.
/// </param>
/// <returns></returns>
public static T AnyOrDefault<T>(this IList<T> e, Func<T, double> weightSelector)
{
    if (e.Count < 1)
        return default(T);
    if (e.Count == 1)
        return e[0];
    var weights = e.Select(o => Math.Max(weightSelector(o), 0)).ToArray();
    var sum = weights.Sum(d => d);

    var rnd = new Random().NextDouble();
    for (int i = 0; i < weights.Length; i++)
    {
        //Normalize weight
        var w = sum == 0
            ? 1 / (double)e.Count
            : weights[i] / sum;
        if (rnd < w)
            return e[i];
        rnd -= w;
    }
    throw new Exception("Should not happen");
}
0
Tom Esterez