Алгоритм генерации последовательности, пропорциональной заданному проценту

Учитывая карту объектов и обозначенные пропорции (скажем, они составляют до 100, чтобы упростить):

val ss : Map[String,Double] = Map("A"->42, "B"->32, "C"->26)

Как создать последовательность, в которой для подмножества размером n будет ~42% "A", ~32% "B" и ~26% "C"? (Очевидно, что маленькие n будут иметь большие ошибки).

(Рабочий язык — Scala, но я просто прошу алгоритм.)

ОБНОВЛЕНИЕ: я отказался от случайного подхода, поскольку, например, вероятность того, что последовательность начнется с AA, составляет ~16%, а вероятность того, что она начнется с BB, составляет ~11%, а вероятность того, что для n точно == (сумма пропорций) распределение было бы идеальным. Итак, следуя ответу @MvG, я реализовал следующее:

/**
Returns the key whose achieved proportions are most below desired proportions
*/
def next[T](proportions : Map[T, Double], achievedToDate : Map[T,Double]) : T = {
    val proportionsSum = proportions.values.sum
    val desiredPercentages = proportions.mapValues(v => v / proportionsSum)
    //Initially no achieved percentages, so avoid / 0 
    val toDateTotal = if(achievedToDate.values.sum == 0.0){
        1
    }else{
        achievedToDate.values.sum
    }
    val achievedPercentages = achievedToDate.mapValues(v => v / toDateTotal)
    val gaps = achievedPercentages.map{ case (k, v) =>
        val gap = desiredPercentages(k) - v
        (k -> gap)
    }
    val maxUnder = gaps.values.toList.sortWith(_ > _).head
    //println("Max gap is " + maxUnder)
    val gapsForMaxUnder = gaps.mapValues{v => Math.abs(v - maxUnder) < Double.Epsilon }
    val keysByHasMaxUnder = gapsForMaxUnder.map(_.swap)
    keysByHasMaxUnder(true)
}

/**
Stream of most-fair next element 
*/
def proportionalStream[T](proportions : Map[T, Double], toDate : Map[T, Double]) : Stream[T] = {
    val nextS = next(proportions, toDate)
    val tailToDate = toDate + (nextS -> (toDate(nextS) + 1.0))
    Stream.cons(
        nextS,
        proportionalStream(proportions, tailToDate)
    )
}

Это при использовании, например, :

val ss : Map[String,Double] = Map("A"->42, "B"->32, "C"->26)
val none : Map[String,Double] = ss.mapValues(_ => 0.0)
val mySequence = (proportionalStream(ss, none) take 100).toList
println("Desired : " + ss)
println("Achieved : " + mySequence.groupBy(identity).mapValues(_.size))
mySequence.map(s => print(s))
println

производит:

Desired : Map(A -> 42.0, B -> 32.0, C -> 26.0)
Achieved : Map(C -> 26, A -> 42, B -> 32)
ABCABCABACBACABACBABACABCABACBACABABCABACABCABACBA
CABABCABACBACABACBABACABCABACBACABABCABACABCABACBA

person Larry OBrien    schedule 10.07.2012    source источник
comment
Ключевые слова для поиска: взвешенный генератор случайных чисел.   -  person Kendall Frey    schedule 11.07.2012
comment
Вы заметили, что ваш вывод отличается от моего, первое отличие находится в позиции 11? У меня есть 5,3,3 как наиболее близкое приближение к предполагаемым количествам: 4,62,3,52,2,86, тогда как у вас есть 4,4,3 как наиболее близкое приближение к пропорциям, с фактическими пропорциями около 36%,36%,27%. Близость в обоих случаях, минимизирующая максимальную разницу («норма бесконечности»). Интерпретаций может быть много…   -  person MvG    schedule 11.07.2012


Ответы (5)


Для детерминированного подхода наиболее очевидным решением, вероятно, будет следующее:

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

Такой подход обеспечит оптимальное соблюдение заданного отношения для каждого префикса бесконечной последовательности, сгенерированной таким образом.

Быстрое и грязное доказательство концепции python (не ожидайте, что какие-либо «имена» переменных будут иметь какой-либо смысл):

import sys

p = [0.42, 0.32, 0.26]
c = [0, 0, 0]
a = ['A', 'B', 'C']
n = 0

while n < 70*5:
    n += 1
    x = 0
    s = n*p[0] - c[0]
    for i in [1, 2]:
        si = n*p[i] - c[i]
        if si > s:
            x = i
            s = si
    sys.stdout.write(a[x])
    if n % 70 == 0:
        sys.stdout.write('\n')
    c[x] += 1

Генерирует

ABCABCABACABACBABCAABCABACBACABACBABCABACABACBACBAABCABCABACABACBABCAB
ACABACBACABACBABCABACABACBACBAABCABCABACABACBABCAABCABACBACABACBABCABA
CABACBACBAABCABCABACABACBABCABACABACBACBAACBABCABACABACBACBAABCABCABAC
ABACBABCABACABACBACBAACBABCABACABACBACBAABCABCABACABACBABCABACABACBACB
AACBABCABACABACBACBAABCABCABACABACBABCAABCABACBACBAACBABCABACABACBACBA
person MvG    schedule 10.07.2012
comment
Это похоже на много циклов для чего-то, что можно было бы определить, просто умножив n на коэффициенты масштабирования и упорядочивая детерминированным образом, чтобы максимально уравнять сумму, потерянную при округлении вверх/вниз. Вы можете сделать простое наивное округление, тогда у вас будет либо слишком большое значение, либо слишком маленькое. Просто корректируйте результаты один за другим в соответствии с пропорциональной частью, которая больше всего отличается, пока сумма не станет правильным числом N. - person ErikE; 11.07.2012
comment
@ErikE, ваш подход, насколько я понимаю, основан на фиксированном числе n, и даже в этом случае я считаю, что эта часть «упорядочения» также потребует некоторого зацикливания. Я стремился к потенциально бесконечной последовательности и только низкоуровневым примитивным операциям без скрытых внутри них нупов. Тем не менее, мне все равно было бы интересно увидеть вашу идею более подробно в качестве отдельного ответа. - person MvG; 11.07.2012
comment
Не правильно, зацикливание да, но только p раз, а не n, и я постараюсь найти время в ближайшее время. - person ErikE; 11.07.2012
comment
@ErikE, какой у тебя p? Если это p = 100, то обратите внимание, что пропорции двойные, а не целые, так что даже последовательность длиной 100 может не точно соответствовать требованию. Чтобы сгенерировать последовательность из n элементов с учетом m различных символов с соответствующими весами, мой алгоритм требует O(nm) шагов. Вы можете заменить внутренний цикл кучей, чтобы уменьшить его до O(n log m). Невозможно опуститься ниже O(n), поскольку временная сложность не может быть меньше длины сгенерированного вывода. - person MvG; 11.07.2012
comment
вы правы, вам нужно перебирать каждый элемент, несмотря ни на что. Я просто надеялся избежать вложенного цикла. Я не уверен, что это возможно. - person ErikE; 11.07.2012
comment
Спасибо, что подвергли меня испытанию. Ты прав! Мой метод, он не работает так кит. Во-первых, ваш вложенный цикл является наилучшим из возможных: он, как вы говорите, O(nm), и элементы не только имеют хорошо распределенный шаблон, но и имеют наименьшую ошибку на каждом шаге. Мой метод отлично справился с очень быстрым вычислением необходимого количества каждого элемента, но потом... как превратить это в детерминированный список элементов в псевдослучайном порядке? У меня есть рабочая программа, но она недетерминирована. Спасибо за обучение меня и простите за потраченное время. - person ErikE; 11.07.2012

Для каждого элемента последовательности вычислите (псевдо)случайное число r, равнораспределенное между 0 (включительно) и 100 (исключительно).

  • Если 0 ≤ r ‹ 42, возьмите A
  • Если 42 ≤ r ‹ (42+32), возьмите B
  • Если (42+32) ≤ r ‹ (42+32+26)=100, возьмите C
person MvG    schedule 10.07.2012
comment
Да, это то, о чем я думал, но неужели нет детерминированного алгоритма? - person Larry OBrien; 11.07.2012
comment
@larsmans Ну, это именно то, о чем я думал! Но я беспокоюсь о справедливости с маленькими n. Нет ли простого алгоритма, гарантирующего столь же справедливую, сколь и разумную работу с небольшим n? - person Larry OBrien; 11.07.2012
comment
@LarryOBrien Я предпочитаю этот ответ, но если вы хотите что-то детерминированное, вам следует использовать ответ Колина Д. - person daniloquio; 11.07.2012
comment
Это может иметь такие вариации, особенно для небольших значений! n=3 может реально дать AAA, что далеко не самое лучшее ABC. Извини, нет. - person ErikE; 11.07.2012

Количество каждой записи в вашем подмножестве будет таким же, как и на вашей карте, но с применением коэффициента масштабирования.

Коэффициент масштабирования равен n/100.

Итак, если бы n было равно 50, у вас было бы { Ax21, Bx16, Cx13 }.

Рандомизируйте заказ по своему вкусу.

person Colin D    schedule 10.07.2012
comment
Я думаю, это то, что ищет ОП. Ему нужно определить четкие и надежные правила для округления результатов, если он хочет получить не слишком странный результат при малом n. - person daniloquio; 11.07.2012
comment
Да, вы описали оптимальную справедливость сгенерированной последовательности, но я надеялся на что-то, что сгенерирует (одну из) наиболее справедливых последовательностей. - person Larry OBrien; 11.07.2012
comment
@LarryOBrien Я не уверен, что вы подразумеваете под «наиболее справедливым», возможно, вы можете уточнить это в своем вопросе. - person Colin D; 11.07.2012
comment
@ColinD Я просто имею в виду, что для большинства распределений и большинства n последовательность не будет идеально пропорциональна желаемым распределениям. - person Larry OBrien; 11.07.2012

Простейшим «детерминированным» [с точки зрения #элементов каждой категории] решением [IMO] будет: добавлять элементы в заданном порядке, а затем перемешивать получившийся список.

Сначала добавьте map(x)/100 * n элементов из каждого элемента x, выбрав способ обработки целочисленной арифметики, чтобы избежать смещения на один элемент], а затем перетасуйте полученный список.

Перетасовать список очень просто с помощью тасования Фишера-Йейтса, реализованного в большинство языков: например, у java есть Collections.shuffle(), а C++ имеет random_shuffle()

В java это будет так же просто, как:

int N = 107;
List<String> res = new ArrayList<String>();
for (Entry<String,Integer> e : map.entrySet()) { //map is predefined Map<String,Integer> for frequencies
    for (int i = 0; i < Math.round(e.getValue()/100.0 * N); i++) {
        res.add(e.getKey());
    }
}
Collections.shuffle(res);
person amit    schedule 10.07.2012

Это недетерминировано, но дает распределение значений, близкое к MvG. Он страдает от проблемы, которая может дать AAA с самого начала. Я размещаю его здесь для полноты картины, учитывая, как это доказывает, что мое несогласие с MvG было неуместным (и я не ожидаю никаких голосов).

Теперь, если у кого-то есть идея для функции expand, которая является детерминированной и не будет просто дублировать метод MvG (делает функцию calc бесполезной), я внимательно слушаю!

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>ErikE's answer</title>
</head>
<body>
<div id="output"></div>
<script type="text/javascript">
if (!Array.each) {
   Array.prototype.each = function(callback) {
      var i, l = this.length;
      for (i = 0; i < l; i += 1) {
         callback(i, this[i]);
      }
   };
}

if (!Array.prototype.sum) {
   Array.prototype.sum = function() {
      var sum = 0;
      this.each(function(i, val) {
         sum += val;
      });
      return sum;
   };
}

function expand(counts) {
   var
      result = "",
      charlist = [],
      l,
      index;
   counts.each(function(i, val) {
      char = String.fromCharCode(i + 65);
      for ( ; val > 0; val -= 1) {
         charlist.push(char);
      }
   });
   l = charlist.length;
   for ( ; l > 0; l -= 1) {
      index = Math.floor(Math.random() * l);
      result += charlist[index];
      charlist.splice(index, 1);
   }
   return result;
}

function calc(n, proportions) {
   var percents = [],
      counts = [],
      errors = [],
      fnmap = [],
      errorSum,
      worstIndex;

   fnmap[1] = "min";
   fnmap[-1] = "max";

   proportions.each(function(i, val) {
      percents[i] = val / proportions.sum() * n;
      counts[i] = Math.round(percents[i]);
      errors[i] = counts[i] - percents[i];
   });

   errorSum = counts.sum() - n;
   while (errorSum != 0) {
      adjust = errorSum < 0 ? 1 : -1;
      worstIndex = errors.indexOf(Math[fnmap[adjust]].apply(0, errors));
      counts[worstIndex] += adjust;
      errors[worstIndex] = counts[worstIndex] - percents[worstIndex];
      errorSum += adjust;
   }
   return expand(counts);
}

document.body.onload = function() {
   document.getElementById('output').innerHTML = calc(99, [25.1, 24.9, 25.9, 24.1]);
};
</script>
</body>
</html>
person ErikE    schedule 11.07.2012