Как сделать округленные проценты добавить до 100%

рассмотрим четыре процента ниже, представленные как float номера:

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%

мне нужно представить эти проценты как целые числа. Если я просто использую Math.round(), Я в конечном итоге в общей сложности 101%.

14 + 48 + 10 + 29 = 101

если я использую parseInt(), Я в конечном итоге в общей сложности 97%.

13 + 47 + 9 + 28 = 97

что такое хороший алгоритм для представления любого количества процентов в виде целых чисел, сохраняя при этом в общей сложности 100%?


редактировать: после прочтения некоторых комментариев и ответов, очевидно, есть много способов решить эту проблему.

на мой взгляд, чтобы оставаться верным числам," правильный " результат-это тот, который минимизирует общую ошибку, определяемую тем, сколько ошибок округления введет относительно фактического значения:

        value  rounded     error               decision
   ----------------------------------------------------
    13.626332       14      2.7%          round up (14)
    47.989636       48      0.0%          round up (48)
     9.596008       10      4.0%    don't round up  (9)
    28.788024       29      2.7%          round up (29)

в случае ничьей (3.33, 3.33, 3.33) может быть принято произвольное решение (например, 3, 4, 3).

16 ответов


поскольку ни один из ответов здесь, похоже, не решает его должным образом, вот моя полу-запутанная версия с использованием underscorejs:

function foo(l, target) {
    var off = target - _.reduce(l, function(acc, x) { return acc + Math.round(x) }, 0);
    return _.chain(l).
            sortBy(function(x) { return Math.round(x) - x }).
            map(function(x, i) { return Math.round(x) + (off > i) - (i >= (l.length + off)) }).
            value();
}

foo([13.626332, 47.989636, 9.596008, 28.788024], 100) // => [48, 29, 14, 9]
foo([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100) // => [17, 17, 17, 17, 16, 16]
foo([33.333, 33.333, 33.333], 100) // => [34, 33, 33]
foo([33.3, 33.3, 33.3, 0.1], 100) // => [34, 33, 33, 0]

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

первым и, возможно, самым популярным методом будет Самый Большой Метод Остатка

который является в основном:

  1. округления все
  2. получение разницы в сумме и 100
  3. распределение разницы путем добавления 1 к элементам в порядке убывания их десятичной дроби части

в вашем случае, это будет выглядеть так:

13.626332%
47.989636%
 9.596008%
28.788024%

если вы берете целочисленные части, вы получаете

13
47
 9
28

что составляет 97, и вы хотите добавить еще три. Теперь вы смотрите на десятичные части, которые являются

.626332%
.989636%
.596008%
.788024%

и взять самых крупных до полного достигает 100. Так вы получите:

14
48
 9
29

кроме того, вы можете просто выбрать, чтобы показать один десятичный знак вместо целых значений. Так что цифры будут быть 48.3 и 23,9 и т. д. Это снизило бы отклонение от 100 на много.


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

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
13.626332   13.626332            14             0    14 ( 14 -  0)
47.989636   61.615968            62            14    48 ( 62 - 14)
 9.596008   71.211976            71            62     9 ( 71 - 62)
28.788024  100.000000           100            71    29 (100 - 71)
                                                    ---
                                                    100

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

это работает, потому что ты не потеря информации на каждом этапе, но вместо того, чтобы использовать информацию более разумно. "Правильные" округленные значения находятся в последнем столбце,и вы можете видеть, что они составляют 100.


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

на хорошо проголосовал ответ Варун Вохра уменьшает сумму абсолютных ошибок, и очень просто осуществлять. Однако есть крайние случаи, которые он не обрабатывает - каким должен быть результат округления 24.25, 23.25, 27.25, 25.25? Один из них должен быть округлен вверх, а не вниз. Вы, вероятно, просто произвольно выберете первый или последний в списке.

возможно, лучше использовать относительные ошибка вместо абсолютное ошибка. Округление 23.25 до 24 изменяет его на 3.2% , а округление 27.25 до 28 изменяет его только на 2.8%. Теперь все ясно. победитель.

можно настроить это еще больше. Один из распространенных методов -площадью каждая ошибка, так что большие ошибки считаются непропорционально больше, чем маленькие. Я бы также использовал нелинейный делитель для получения относительной ошибки - кажется неправильным, что ошибка в 1% в 99 раз важнее, чем ошибка в 99%. В приведенном ниже коде я использовал квадратный корень.

полный алгоритм выглядит следующим образом:

  1. сумма в процент после округления их всех вниз, и вычесть из 100. Это говорит вам, сколько из этих процентов должны быть округлены вместо этого.
  2. генерировать два балла ошибок для каждого процента, один при округлении вниз и один при округлении вверх. Возьмите разницу между ними.
  3. сортировка различий ошибок, произведенных выше.
  4. для количества процентов, которые должны быть округлены, возьмите элемент из отсортированного списка и увеличьте округленный вниз процент от 1.

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

объединение всего этого в Python выглядит так.

def error_gen(actual, rounded):
    divisor = sqrt(1.0 if actual < 1.0 else actual)
    return abs(rounded - actual) ** 2 / divisor

def round_to_100(percents):
    if not isclose(sum(percents), 100):
        raise ValueError
    n = len(percents)
    rounded = [int(x) for x in percents]
    up_count = 100 - sum(rounded)
    errors = [(error_gen(percents[i], rounded[i] + 1) - error_gen(percents[i], rounded[i]), i) for i in range(n)]
    rank = sorted(errors)
    for i in range(up_count):
        rounded[rank[i][1]] += 1
    return rounded

>>> round_to_100([13.626332, 47.989636, 9.596008, 28.788024])
[14, 48, 9, 29]
>>> round_to_100([33.3333333, 33.3333333, 33.3333333])
[34, 33, 33]
>>> round_to_100([24.25, 23.25, 27.25, 25.25])
[24, 23, 28, 25]
>>> round_to_100([1.25, 2.25, 3.25, 4.25, 89.0])
[1, 2, 3, 4, 90]

как вы можете видеть в этом последнем примере, этот алгоритм по-прежнему способен доставлять неинтуитивные результаты. Хотя 89.0 нуждается при любом округлении одно из значений в этом списке должно быть округлено; наименьшая относительная ошибка является результатом округления этого большого значения, а не гораздо меньших альтернатив.

этот ответ первоначально выступал за то, чтобы пройти все возможные комбинации round up/round down, но, как указано в комментариях, более простой метод работает лучше. Алгоритм и код отражают это упрощение.


не суммируйте округленные числа. У вас будут неточные результаты. Общая сумма может быть значительно меньше в зависимости от количества терминов и распределения дробных частей.

дисплей округленные цифры, но sum фактические значения. В зависимости от того, как вы представляете цифры, фактический способ сделать это будет отличаться. Таким образом, вы получите

 14
 48
 10
 29
 __
100

любой путь вы идете вы идете несоответствие. В вашем примере нет способа показать числа, которые складываются до 100 без" округления " одного значения неправильным образом (наименьшая ошибка будет изменять 9.596 на 9)

редактировать

вам нужно выбрать один из следующих вариантов:

  1. точность элементов
  2. точность суммы (если вы суммируете округленные значения)
  3. согласованность между округленными элементами и округленной суммой)

большую часть времени при работе с процентами #3 является лучшим вариантом, потому что это более очевидно, когда общая сумма равна 101%, чем когда отдельные элементы не составляют 100, и вы держите отдельные элементы точными. "Округление" 9.596 до 9, на мой взгляд, неточно.

чтобы объяснить это, я иногда добавляю сноску, которая объясняет, что отдельные значения округлены и не могут составлять 100% - любой, кто понимает округление, должен быть в состоянии понять это объяснение.


Я написал помощник округления версии C#, алгоритм такой же, как ответ Варуна вохры, надеюсь, что это помогает.

public static List<decimal> GetPerfectRounding(List<decimal> original,
    decimal forceSum, int decimals)
{
    var rounded = original.Select(x => Math.Round(x, decimals)).ToList();
    Debug.Assert(Math.Round(forceSum, decimals) == forceSum);
    var delta = forceSum - rounded.Sum();
    if (delta == 0) return rounded;
    var deltaUnit = Convert.ToDecimal(Math.Pow(0.1, decimals)) * Math.Sign(delta);

    List<int> applyDeltaSequence; 
    if (delta < 0)
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderBy(a => original[a.index] - rounded[a.index])
            .ThenByDescending(a => a.index)
            .Select(a => a.index).ToList();
    }
    else
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderByDescending(a => original[a.index] - rounded[a.index])
            .Select(a => a.index).ToList();
    }

    Enumerable.Repeat(applyDeltaSequence, int.MaxValue)
        .SelectMany(x => x)
        .Take(Convert.ToInt32(delta/deltaUnit))
        .ForEach(index => rounded[index] += deltaUnit);

    return rounded;
}

он проходит следующий модульный тест:

[TestMethod]
public void TestPerfectRounding()
{
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 2),
        new List<decimal> {3.33m, 3.34m, 3.33m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.33m, 3.34m, 3.33m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});


    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 13.626332m, 47.989636m, 9.596008m, 28.788024m }, 100, 0),
        new List<decimal> {14, 48, 9, 29});
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 16.666m, 16.666m, 16.666m, 16.666m, 16.666m, 16.666m }, 100, 0),
        new List<decimal> { 17, 17, 17, 17, 16, 16 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.333m, 33.333m, 33.333m }, 100, 0),
        new List<decimal> { 34, 33, 33 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.3m, 33.3m, 33.3m, 0.1m }, 100, 0),
        new List<decimal> { 34, 33, 33, 0 });
}

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

13.62 -> 14 (+.38)
47.98 -> 48 (+.02 (+.40 total))
 9.59 -> 10 (+.41 (+.81 total))
28.78 -> 28 (round down because .81 > .78)
------------
        100

не уверен, что это будет работать в целом, но, похоже, работает аналогично, если порядок отменен:

28.78 -> 29 (+.22)
 9.59 ->  9 (-.37; rounded down because .59 > .22)
47.98 -> 48 (-.35)
13.62 -> 14 (+.03)
------------
        100

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


однажды я написал инструмент unround, чтобы найти минимальное возмущение для набора чисел, чтобы соответствовать цели. Это была другая проблема, но теоретически можно было бы использовать подобную идею здесь. В этом случае у нас есть выбор.

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

  13.626332
  47.989636
   9.596008
+ 28.788024
-----------
 100.000000

обычно я решаю общую проблему в MATLAB, здесь используя bintprog, инструмент двоичного целочисленного программирования, но есть только несколько вариантов для тестирования, поэтому достаточно легко с помощью простых циклов протестировать каждую из 16 альтернатив. Например, предположим, что мы должны были бы округлить это множество как:

 Original      Rounded   Absolute error
   13.626           13          0.62633
    47.99           48          0.01036
    9.596           10          0.40399
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.25266

общая абсолютная ошибка составляет 1.25266. Это может быть несколько уменьшено следующим альтернативным округлением:

 Original      Rounded   Absolute error
   13.626           14          0.37367
    47.99           48          0.01036
    9.596            9          0.59601
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.19202

на самом деле, это будет оптимальное решение с точки зрения абсолютной ошибки. Конечно, если бы было 20 терминов, пространство поиска было бы размером 2^20 = 1048576. В течение 30 или 40 сроков это пространство будет иметь значительные размеры. В этом случае вам нужно будет использовать инструмент, который может эффективно искать пространство, возможно, используя ветвь и связанную схему.


Я думаю, что следующее достигнет того, что вы после

function func( orig, target ) {

    var i = orig.length, j = 0, total = 0, change, newVals = [], next, factor1, factor2, len = orig.length, marginOfErrors = [];

    // map original values to new array
    while( i-- ) {
        total += newVals[i] = Math.round( orig[i] );
    }

    change = total < target ? 1 : -1;

    while( total !== target ) {

        // Iterate through values and select the one that once changed will introduce
        // the least margin of error in terms of itself. e.g. Incrementing 10 by 1
        // would mean an error of 10% in relation to the value itself.
        for( i = 0; i < len; i++ ) {

            next = i === len - 1 ? 0 : i + 1;

            factor2 = errorFactor( orig[next], newVals[next] + change );
            factor1 = errorFactor( orig[i], newVals[i] + change );

            if(  factor1 > factor2 ) {
                j = next; 
            }
        }

        newVals[j] += change;
        total += change;
    }


    for( i = 0; i < len; i++ ) { marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i]; }

    // Math.round() causes some problems as it is difficult to know at the beginning
    // whether numbers should have been rounded up or down to reduce total margin of error. 
    // This section of code increments and decrements values by 1 to find the number
    // combination with least margin of error.
    for( i = 0; i < len; i++ ) {
        for( j = 0; j < len; j++ ) {
            if( j === i ) continue;

            var roundUpFactor = errorFactor( orig[i], newVals[i] + 1)  + errorFactor( orig[j], newVals[j] - 1 );
            var roundDownFactor = errorFactor( orig[i], newVals[i] - 1) + errorFactor( orig[j], newVals[j] + 1 );
            var sumMargin = marginOfErrors[i] + marginOfErrors[j];

            if( roundUpFactor < sumMargin) { 
                newVals[i] = newVals[i] + 1;
                newVals[j] = newVals[j] - 1;
                marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
            }

            if( roundDownFactor < sumMargin ) { 
                newVals[i] = newVals[i] - 1;
                newVals[j] = newVals[j] + 1;
                marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
            }

        }
    }

    function errorFactor( oldNum, newNum ) {
        return Math.abs( oldNum - newNum ) / oldNum;
    }

    return newVals;
}


func([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100); // => [16, 16, 17, 17, 17, 17]
func([33.333, 33.333, 33.333], 100); // => [34, 33, 33]
func([33.3, 33.3, 33.3, 0.1], 100); // => [34, 33, 33, 0] 
func([13.25, 47.25, 11.25, 28.25], 100 ); // => [13, 48, 11, 28]
func( [25.5, 25.5, 25.5, 23.5], 100 ); // => [25, 25, 26, 24]

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

func([13.626332, 47.989636, 9.596008, 28.788024], 100); // => [48, 29, 13, 10]

это отличалось от того, что хотел вопрос => [ 48, 29, 14, 9]. Я не мог понять этого, пока не посмотрел на общую погрешность

-------------------------------------------------
| original  | question | % diff | mine | % diff |
-------------------------------------------------
| 13.626332 | 14       | 2.74%  | 13   | 4.5%   |
| 47.989636 | 48       | 0.02%  | 48   | 0.02%  |
| 9.596008  | 9        | 6.2%   | 10   | 4.2%   |
| 28.788024 | 29       | 0.7%   | 29   | 0.7%   |
-------------------------------------------------
| Totals    | 100      | 9.66%  | 100  | 9.43%  |
-------------------------------------------------

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

Скрипка здесь


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

вы можете взять десятичную часть N процентов, которые у вас есть (в приведенном примере это 4).

добавить десятичные части. В вашем примере у вас есть всего дробная часть = 3.

Ceil 3 числа с самыми высокими дробями и полом остальные.

(извините за правки)


Я не уверен, какой уровень точности вам нужен, Но я бы просто добавил 1 первый n цифры n будучи ceil общей суммы десятичных дробей. В этом случае это 3, поэтому я бы добавил 1 к первым 3 пунктам и Наполеон остальные. Конечно, это не очень точно, некоторые цифры могут быть округлены вверх или вниз, когда это не должно, но это работает нормально и всегда приведет к 100%.

так [ 13.626332, 47.989636, 9.596008, 28.788024 ] будет [14, 48, 10, 28] потому что Math.ceil(.626332+.989636+.596008+.788024) == 3

function evenRound( arr ) {
  var decimal = -~arr.map(function( a ){ return a % 1 })
    .reduce(function( a,b ){ return a + b }); // Ceil of total sum of decimals
  for ( var i = 0; i < decimal; ++i ) {
    arr[ i ] = ++arr[ i ]; // compensate error by adding 1 the the first n items
  }
  return arr.map(function( a ){ return ~~a }); // floor all other numbers
}

var nums = evenRound( [ 13.626332, 47.989636, 9.596008, 28.788024 ] );
var total = nums.reduce(function( a,b ){ return a + b }); //=> 100

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


Если вы действительно должны их обойти, здесь уже есть очень хорошие предложения (самый большой остаток, наименьшая относительная ошибка и т. д.).

есть также уже одна веская причина не округлять (вы получите хотя бы одно число, которое "выглядит лучше", но "неправильно"), и как это решить (предупредить ваших читателей), и это то, что я делаю.

позвольте мне добавить "неправильную" часть номера.

предположим, что у вас есть три события/entitys/... с некоторыми процентами, что вы приблизительно как:

DAY 1
who |  real | app
----|-------|------
  A | 33.34 |  34
  B | 33.33 |  33
  C | 33.33 |  33

позже значения немного изменятся, до

DAY 2
who |  real | app
----|-------|------
  A | 33.35 |  33
  B | 33.36 |  34
  C | 33.29 |  33

первая таблица имеет уже упомянутую проблему "неправильная" номер: 33.34 ближе к 33, чем к 34.

но теперь у вас есть большие ошибки. Сравнивая день 2 С днем 1, реальное процентное значение для A увеличилось на 0,01%, но приближение показывает снижение на 1%.

это качественная ошибка, вероятно, гораздо хуже, чем исходная количественная ошибка.

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


Это случай округления банкира, он же "круглая половина четная". Он поддерживается BigDecimal. Его цель-обеспечить, чтобы округление остатков, т. е. не благоприятствовало ни банку, ни клиенту.


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

предположим, что число k;

  1. сортировать процент по убыванию oder.
  2. перебирать каждый процент от нисходящего порядка.
  3. вычислить процент k для первого процента взять математику.Ceil выхода.
  4. следующий k = k-1
  5. повторите, пока весь процент не будет потреблен.

я реализовал метод из ответа Варуна вохры здесь для обоих списков и диктов.

import math
import numbers
import operator
import itertools


def round_list_percentages(number_list):
    """
    Takes a list where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    if not all(isinstance(i, numbers.Number) for i in number_list):
        raise ValueError('All values of the list must be a number')

    # Generate a key for each value
    key_generator = itertools.count()
    value_dict = {next(key_generator): value for value in number_list}
    return round_dictionary_percentages(value_dict).values()


def round_dictionary_percentages(dictionary):
    """
    Takes a dictionary where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    # Only allow numbers
    if not all(isinstance(i, numbers.Number) for i in dictionary.values()):
        raise ValueError('All values of the dictionary must be a number')
    # Make sure the sum is close enough to 100
    # Round value_sum to 2 decimals to avoid floating point representation errors
    value_sum = round(sum(dictionary.values()), 2)
    if not value_sum == 100:
        raise ValueError('The sum of the values must be 100')

    # Initial floored results
    # Does not add up to 100, so we need to add something
    result = {key: int(math.floor(value)) for key, value in dictionary.items()}

    # Remainders for each key
    result_remainders = {key: value % 1 for key, value in dictionary.items()}
    # Keys sorted by remainder (biggest first)
    sorted_keys = [key for key, value in sorted(result_remainders.items(), key=operator.itemgetter(1), reverse=True)]

    # Otherwise add missing values up to 100
    # One cycle is enough, since flooring removes a max value of < 1 per item,
    # i.e. this loop should always break before going through the whole list
    for key in sorted_keys:
        if sum(result.values()) == 100:
            break
        result[key] += 1

    # Return
    return result

вот более простая реализация Python @varun-vohra ответ:

def apportion_pcts(pcts, total):
    proportions = [total * (pct / 100) for pct in pcts]
    apportions = [math.floor(p) for p in proportions]
    remainder = total - sum(apportions)
    remainders = [(i, p - math.floor(p)) for (i, p) in enumerate(proportions)]
    remainders.sort(key=operator.itemgetter(1), reverse=True)
    for (i, _) in itertools.cycle(remainders):
        if remainder == 0:
            break
        else:
            apportions[i] += 1
            remainder -= 1
    return apportions

вам нужно math, itertools, operator.