Пропорционально распределить (пропорционально) значение по набору значений - PullRequest
20 голосов
/ 18 декабря 2009

Мне нужно написать код, который будет пропорционально распределять значения по списку, основываясь на относительных весах «базовых» значений в списке. Простое деление «базовых» значений на сумму «базовых» значений, а затем умножение коэффициента на исходное значение для пропорциональной работы в определенной степени:

proratedValue = (basis / basisTotal) * prorationAmount;

Однако результат этого вычисления должен быть затем округлен до целых значений. Эффект округления означает, что сумма proratedValue для всех элементов в списке может отличаться от первоначальной суммы prorationAmount.

Может ли кто-нибудь объяснить, как применять алгоритм пропорционального распределения «без потерь», который пропорционально распределяет значение по списку с максимально возможной точностью, не страдая от ошибок округления?

Ответы [ 6 ]

16 голосов
/ 18 декабря 2009

Простой алгоритм зарисовки здесь ...

  1. Иметь промежуточный итог, который начинается с нуля.
  2. Выполните стандартное «деление на общую сумму, а затем умножьте на пропорцию» для первого элемента.
  3. Сохраните исходное значение промежуточного итога в другом месте, затем добавьте сумму, которую вы только что рассчитали, в # 2.
  4. Округлите и старое значение, и новое значение промежуточного итога до целых чисел (не изменяйте существующие значения, округляйте их в отдельные переменные) и принимайте разницу.
  5. Число, вычисленное на шаге 4, является значением, присвоенным текущей основе.
  6. Повторите шаги # 2-5 для каждой основы.

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

Базовый пример:

Input basis: [0.2, 0.3, 0.3, 0.2]
Total prorate: 47

----

R used to indicate running total here:

R = 0

First basis:
  oldR = R [0]
  R += (0.2 / 1.0 * 47) [= 9.4]
  results[0] = int(R) - int(oldR) [= 9]

Second basis:
  oldR = R [9.4]
  R += (0.3 / 1.0 * 47) [+ 14.1, = 23.5 total]
  results[1] = int(R) - int(oldR) [23-9, = 14]

Third basis:
  oldR = R [23.5]
  R += (0.3 / 1.0 * 47) [+ 14.1, = 37.6 total]
  results[1] = int(R) - int(oldR) [38-23, = 15]

Fourth basis:
  oldR = R [37.6]
  R += (0.2 / 1.0 * 47) [+ 9.4, = 47 total]
  results[1] = int(R) - int(oldR) [47-38, = 9]

9+14+15+9 = 47
11 голосов
/ 11 октября 2012

TL; DR алгоритм с наилучшей (+ 20%) возможной точностью, на 70% медленнее.

Эвакуированные алгоритмы представлены в принятом ответе здесь , а также ответ на вопрос Python аналогичного характера.

  • Распределение 1 - на основе Алгоритм Амбер
  • Распространение 2 - на основе Алгоритм Джона Мачина
  • Распределить 3 - см. Ниже
  • Распределение 4 - оптимизированная версия Распределение 3 (например, удаленные LINQ, использованные массивы)

Результаты тестирования (10 000 итераций)

Algorithm    | Avg Abs Diff (x lowest) | Time (x lowest)     
------------------------------------------------------------------
Distribute 1 | 0.5282 (1.1992)         | 00:00:00.0906921 (1.0000)
Distribute 2 | 0.4526 (1.0275)         | 00:00:00.0963136 (1.0620)
Distribute 3 | 0.4405 (1.0000)         | 00:00:01.1689239 (12.8889)
Distribute 4 | 0.4405 (1.0000)         | 00:00:00.1548484 (1.7074)

Точность метода 3 выше на 19,9%, а время выполнения на 70,7% меньше ожидаемого.

Распределить 3

Прилагает все усилия, чтобы быть как можно точнее в распределении суммы.

  1. Распределить веса как обычно
  2. Увеличение веса с наибольшая ошибка , пока фактическая распределенная сумма не станет равной ожидаемой сумме

жертвует скоростью ради точности, делая более одного прохода по петле.

public static IEnumerable<int> Distribute3(IEnumerable<double> weights, int amount)
{
    var totalWeight = weights.Sum();
    var query = from w in weights
                let fraction = amount * (w / totalWeight)
                let integral = (int)Math.Floor(fraction)
                select Tuple.Create(integral, fraction);

    var result = query.ToList();
    var added = result.Sum(x => x.Item1);

    while (added < amount)
    {
        var maxError = result.Max(x => x.Item2 - x.Item1);
        var index = result.FindIndex(x => (x.Item2 - x.Item1) == maxError);
        result[index] = Tuple.Create(result[index].Item1 + 1, result[index].Item2);
        added += 1;
    }

    return result.Select(x => x.Item1);
}

Распределить 4

public static IEnumerable<int> Distribute4(IEnumerable<double> weights, int amount)
{
    var totalWeight = weights.Sum();
    var length = weights.Count();

    var actual = new double[length];
    var error = new double[length];
    var rounded = new int[length];

    var added = 0;

    var i = 0;
    foreach (var w in weights)
    {
        actual[i] = amount * (w / totalWeight);
        rounded[i] = (int)Math.Floor(actual[i]);
        error[i] = actual[i] - rounded[i];
        added += rounded[i];
        i += 1;
    }

    while (added < amount)
    {
        var maxError = 0.0;
        var maxErrorIndex = -1;
        for(var e = 0; e  < length; ++e)
        {
            if (error[e] > maxError)
            {
                maxError = error[e];
                maxErrorIndex = e;
            }
        }

        rounded[maxErrorIndex] += 1;
        error[maxErrorIndex] -= 1;

        added += 1;
    }

    return rounded;
}

Испытательный жгут

static void Main(string[] args)
{
    Random r = new Random();

    Stopwatch[] time = new[] { new Stopwatch(), new Stopwatch(), new Stopwatch(), new Stopwatch() };

    double[][] results = new[] { new double[Iterations], new double[Iterations], new double[Iterations], new double[Iterations] };

    for (var i = 0; i < Iterations; ++i)
    {
        double[] weights = new double[r.Next(MinimumWeights, MaximumWeights)];
        for (var w = 0; w < weights.Length; ++w)
        {
            weights[w] = (r.NextDouble() * (MaximumWeight - MinimumWeight)) + MinimumWeight;
        }
        var amount = r.Next(MinimumAmount, MaximumAmount);

        var totalWeight = weights.Sum();
        var expected = weights.Select(w => (w / totalWeight) * amount).ToArray();

        Action<int, DistributeDelgate> runTest = (resultIndex, func) =>
            {
                time[resultIndex].Start();
                var result = func(weights, amount).ToArray();
                time[resultIndex].Stop();

                var total = result.Sum();

                if (total != amount)
                    throw new Exception("Invalid total");

                var diff = expected.Zip(result, (e, a) => Math.Abs(e - a)).Sum() / amount;

                results[resultIndex][i] = diff;
            };

        runTest(0, Distribute1);
        runTest(1, Distribute2);
        runTest(2, Distribute3);
        runTest(3, Distribute4);
    }
}
2 голосов
/ 09 февраля 2010

Хорошо. Я почти уверен, что оригинальный алгоритм (как написано) и опубликованный код (как написано) не совсем отвечают на почту для теста, описанного @Mathias.

Мое предполагаемое использование этого алгоритма - немного более конкретное применение. Вместо вычисления% с использованием (@amt / @SumAmt), как показано в исходном вопросе. У меня есть фиксированная сумма в долларах, которую нужно разделить или распределить по нескольким элементам на основе% разделения, определенного для каждого из этих элементов. % Разделения составляет 100%, однако, прямое умножение часто приводит к десятичным числам, которые (когда вынуждены округлять до целых $) не составляют общую сумму, которую я разделяю. В этом суть проблемы.

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

Возьмите 100 долларов и разделите его на 3 части, используя 33,333333% в качестве процента.

Использование кода, отправленного @jtw (при условии, что это точная реализация оригинального алгоритма), дает неверный ответ о выделении 33 долл. США для каждого элемента (в результате общая сумма составляет 99 долл.), Поэтому он не проходит тест.

Я думаю, что более точный алгоритм может быть:

  • Иметь промежуточную сумму, которая начинается с 0
  • Для каждого элемента в группе:
  • Рассчитать сумму округленного распределения как ( [Amount to be Split] * [% to Split] )
  • Рассчитать совокупный остаток как [Remainder] + ( [UnRounded Amount] - [Rounded Amount] )
  • Если Round( [Remainder], 0 ) > 1 ИЛИ текущий элемент является ПОСЛЕДНИМ ПУНКТОМ в списке, тогда установите выделение элемента = [Rounded Amount] + Round( [Remainder], 0 )
  • еще установить распределение элемента = [Rounded Amount]
  • Повторите для следующего элемента

Реализован в T-SQL, выглядит так:

-- Start of Code --
Drop Table #SplitList
Create Table #SplitList ( idno int , pctsplit decimal(5, 4), amt int , roundedAmt int )

-- Test Case #1
--Insert Into #SplitList Values (1, 0.3333, 100, 0)
--Insert Into #SplitList Values (2, 0.3333, 100, 0)
--Insert Into #SplitList Values (3, 0.3333, 100, 0)

-- Test Case #2
--Insert Into #SplitList Values (1, 0.20, 57, 0)
--Insert Into #SplitList Values (2, 0.20, 57, 0)
--Insert Into #SplitList Values (3, 0.20, 57, 0)
--Insert Into #SplitList Values (4, 0.20, 57, 0)
--Insert Into #SplitList Values (5, 0.20, 57, 0)

-- Test Case #3
--Insert Into #SplitList Values (1, 0.43, 10, 0)
--Insert Into #SplitList Values (2, 0.22, 10, 0)
--Insert Into #SplitList Values (3, 0.11, 10, 0)
--Insert Into #SplitList Values (4, 0.24, 10, 0)

-- Test Case #4
Insert Into #SplitList Values (1, 0.50, 75, 0)
Insert Into #SplitList Values (2, 0.50, 75, 0)

Declare @R Float
Declare @Results Float
Declare @unroundedAmt Float
Declare @idno Int
Declare @roundedAmt Int
Declare @amt Float
Declare @pctsplit Float
declare @rowCnt int

Select @R = 0
select @rowCnt = 0

-- Define the cursor 
Declare SplitList Cursor For 
Select idno, pctsplit, amt, roundedAmt From #SplitList Order By amt Desc
-- Open the cursor
Open SplitList

-- Assign the values of the first record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
-- Loop through the records
While @@FETCH_STATUS = 0

Begin
    -- Get derived Amounts from cursor
    select @unroundedAmt = ( @amt * @pctsplit )
    select @roundedAmt = Round( @unroundedAmt, 0 )

    -- Remainder
    Select @R = @R + @unroundedAmt - @roundedAmt
    select @rowCnt = @rowCnt + 1

    -- Magic Happens!  (aka Secret Sauce)
    if ( round(@R, 0 ) >= 1 ) or ( @@CURSOR_ROWS = @rowCnt ) Begin
        select @Results = @roundedAmt + round( @R, 0 )
        select @R = @R - round( @R, 0 )
    End
    else Begin
        Select @Results = @roundedAmt
    End

    If Round(@Results, 0) <> 0
    Begin
        Update #SplitList Set roundedAmt = @Results Where idno = @idno
    End

    -- Assign the values of the next record
    Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
End

-- Close the cursor
Close SplitList
Deallocate SplitList

-- Now do the check
Select * From #SplitList
Select Sum(roundedAmt), max( amt ), 
case when max(amt) <> sum(roundedamt) then 'ERROR' else 'OK' end as Test 
From #SplitList

-- End of Code --

Что дает окончательный набор результатов для контрольного примера:

idno   pctsplit   amt     roundedAmt
1      0.3333    100     33
2      0.3333    100     34
3      0.3333    100     33

Насколько я могу судить (и у меня есть несколько тестовых случаев в коде), это обрабатывает все эти ситуации довольно изящно.

2 голосов
/ 18 декабря 2009

Ваша проблема состоит в том, чтобы определить, что такое «приемлемая» политика округления или, другими словами, что вы пытаетесь минимизировать. Рассмотрим сначала эту ситуацию: у вас есть только 2 идентичных элемента в вашем списке, и вы пытаетесь выделить 3 единицы. В идеале вы хотели бы выделить одинаковое количество для каждого элемента (1.5), но этого явно не произойдет. Лучшее, что вы могли бы сделать, это выделить 1 и 2 или 2 и 1. Так что

  • может быть несколько решений для каждого распределения
  • идентичные предметы могут не получить идентичное распределение

Затем я выбрал 1 и 2 вместо 0 и 3, потому что я предполагаю, что вы хотите минимизировать разницу между идеальным распределением и целочисленным распределением. Возможно, это не то, что вы считаете «хорошим распределением», и вам нужно подумать над этим вопросом: что сделает распределение лучше другого?
Одна из возможных функций значения может заключаться в минимизации «общей ошибки», то есть суммы абсолютных значений разностей между вашим распределением и «совершенным» неограниченным распределением.
Мне кажется, что что-то, вдохновленное Branch and Bound , может работать, но это не тривиально.
Предполагая, что решение Dav всегда производит распределение, которое удовлетворяет ограничению (которому я доверяю в данном случае), я предполагаю, что оно не гарантирует, что вы получите «лучшее» решение, «наилучшее», определенное любой метрикой расстояния / соответствия, которую вы в конечном итоге принять. Моя причина в том, что это жадный алгоритм, который в задачах целочисленного программирования может привести вас к решениям, которые действительно не соответствуют оптимальному решению. Но если вы можете жить с «несколько правильным» распределением, то я говорю, сделайте это! Делать это «оптимально» не кажется тривиальным.
Желаем удачи!

1 голос
/ 16 августа 2011

Это проблема пропорционального распределения , для которой существует много известных методов. У всех есть определенные патологии: парадокс Алабамы, парадокс населения или провал правила квот. (Балински и Янг доказали, что ни один метод не может избежать всех трех.) Возможно, вам понадобится метод, который следует правилу цитат и избегает парадокса Алабамы; парадокс в области народонаселения не вызывает особого беспокойства, так как разницы в количестве дней в месяце между годами не так много.

0 голосов
/ 23 мая 2016

Я думаю, что пропорциональные распределения - это ответ: http://www.sangakoo.com/en/unit/proportional-distributions-direct-and-inverse

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...