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


193

Рассмотрим четыре процента ниже, представленные в виде 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).


21
Предположим, у вас есть 3.33, 3.33 и 3.33. Какой из них вы сделаете 4?
RobG

3
Именно. Вопрос заключает в себе противоречие в терминах.
Маркиз Лорн

4
Это очень распространенный сценарий составления отчетов - как отображать «сумму» десятичных значений, которая не всегда соответствует сумме отображаемых значений.
Д Стэнли

1
Каков «правильный» результат в вашем случае? Это может разрешить разногласия по поводу того, что является «лучшим» решением.
D Стэнли

Ответы:


35

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

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]

6
Поправьте меня, если я ошибаюсь, но разве это не реализация алгоритма, предложенного моим ответом? (Не
уточнять

@VarunVohra извините, я не замечал этого до сих пор, да, похоже, ваш алгоритм такой же :) не уверен, почему мой пост является принятым ответом, запутанный код был только для lolz ...
yonilevy

@yonilevy удалил мой комментарий; Я просто не осознавал, что должен был вернуть отсортированный список. Я извиняюсь!
Зак Берт

2
С этой функцией возникает проблема, когда последний элемент равен 0, а предыдущие добавляются к 100. Например, [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Последний логически возвращает -1. Я очень быстро подумал о следующем решении, но, возможно, есть что-то лучше: jsfiddle.net/0o75bw43/1
Cruclax,

1
@Cruclax показывает все 1, когда все записи равны нулю во входном массиве
tony.0919

159

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

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

Что в основном:

  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.


5
В этой «Колонке функций» на веб-сайте Американского математического общества - Распределение II: Системы распределения - описаны несколько похожих методов «распределения».
Кенни Эвитт

1
Это выглядит почти как копия и вставка моего ответа здесь stackoverflow.com/questions/5227215/… .
Савва

Обратите внимание, что, вопреки вашему комментарию к ответу @DStanley, в вашем ответе 9,596008% было округлено до 9%, что составляет разницу более 0,5%. Тем не менее, хороший ответ.
Rolazaro Azeveires

33

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

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

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

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

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

Вы можете увидеть разницу между этим и слепым округлением каждого значения в третьем значении выше. Хотя 9.596008обычно округляется до 10, накопленное 71.211976правильно округляется до 71- это означает, что 9нужно только добавить к предыдущему базовому значению 62.


Это также работает для «проблемной» последовательности, например трех грубых значений, где одно из них должно быть округлено:1/3

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
33.333333   33.333333            33             0    33 ( 33 -  0)
33.333333   66.666666            67            33    34 ( 67 - 33)
33.333333   99.999999           100            67    33 (100 - 67)
                                                    ---
                                                    100

1
Второй подход решает обе эти проблемы. Первый дает 26, 25, 26, 23, второй 1, 0, 1, 0, 1, 0, ....
paxdiablo

Этот подход также хорошо подходит для округления небольших чисел, поскольку он предотвращает отрицательное число в выводе
Jonty5817

19

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

Хорошо проголосовали ответ на Varun Вохра минимизирует сумму абсолютных ошибок, и это очень просто реализовать. Однако есть крайние случаи, которые он не обрабатывает - что должно быть результатом округления 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 не нуждается ни в каком округлении, одно из значений в этом списке необходимо округлить; самая низкая относительная ошибка является результатом округления этого большого значения, а не намного меньших альтернатив.

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


1
Я не думаю, что вам нужно рассматривать все комбинации: процесс в порядке убывания падения взвешенной ошибки, переходящий от раунда к нулю, к раунду до бесконечности (в значительной степени просто введение взвешивания в ответы Веруна Вохраса и Йонилеви («идентичные»)).
седобородый

@greybeard вы правы, я думал об этом. Я не мог просто отсортировать ошибки, так как для каждого значения есть две ошибки, но, учитывая разницу, я решил эту проблему. Я обновил ответ.
Марк Рэнсом

Я предпочитаю всегда иметь 0%, когда фактическое число составляет 0%. Таким образом, добавление if actual == 0: return 0к error_genработе прекрасно.
Николай Балук

1
какой iscloseметод в начале round_to_100?
toto_tico


7

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

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

 14
 48
 10
 29
 __
100

В любом случае вы будете иметь расхождение. В вашем примере нет способа показать числа, которые складываются до 100, без «округления» одного значения в неправильном направлении (наименьшая ошибка изменила бы 9,596 на 9)

РЕДАКТИРОВАТЬ

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

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

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

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


6
Это не очень полезно, так как напечатанные значения не могут составлять до 100. Цель вопроса состояла в том, чтобы не дать пользователям думать, что значения неверны, что в этом случае большинство людей сделали бы при просмотре и сравнении с общим ,
vvohra87

@VarunVohra читайте мои правки, вы НЕ МОЖЕТЕ отображать свои числа так, чтобы они складывались до 100 без «округления» одного более чем на 0,5.
Д Стэнли

1
На самом деле @DStanley, если не считать множества, где все числа стесняются 0,5, вы можете. Проверьте мой ответ - LRM делает именно это.
vvohra87

3
@VarunVohra В исходном примере LRM даст 14, 48, 9 и 29, которые «округляют» 9,596 до 9. Если мы выделяем на основе целых чисел, LRM будет наиболее точным, но он все еще меняет один результат на более чем половина единицы.
D Стэнли

7

Я написал помощник по округлению версии 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 });
}

Ницца! дал мне основание для начала. У Enumerable нет ForEach, хотя я верю
Jack0fshad0ws

4

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

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

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


2
Бухгалтеры и банкиры используют подобную технику на протяжении сотен лет. «Неси остаток» из одного ряда в другой. Начните с 1/2 цента в «нести». Добавьте «перенос» к первому значению и обрежьте. Теперь сумму, которую вы потеряли в результате усечения, поместите это в «перенос». Сделайте это до конца, и округленные числа будут складываться до желаемой суммы точно каждый раз.
Джефф Григг

Кэролайн Кей предложила эту реализацию в Access VB 2007: <code> 'округлить возврат денег с использованием метода «нести остаток» ref1 = rsQry! [Refund Paid $$$] * rsQry! [Значение свойства] / propValTot ref2 = ref1 + ref5 'Добавьте оставшийся остаток, ноль для начала ref3 = ref2 * 100' Умножьте на 100 целое число ref4 = ref3 / 100 'Разделите на 100 десятичное число rsTbl! [Refund Paid $$$] = ref4' Поместите " остаток "округленное число в таблице ref5 = ref2 - ref4 'Перенос нового остатка </ code>
Джефф Григг

2

Однажды я написал инструмент 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 терминов это пространство будет иметь значительный размер. В этом случае вам нужно будет использовать инструмент, который может эффективно искать пространство, возможно, используя схему ветвей и границ.


Просто для дальнейшего использования: алгоритм «наибольшего остатка» должен сводить к минимуму суммарную абсолютную ошибку в соответствии с вашей метрикой (см. Ответ @ varunvohra). Доказательство простое: предположим, оно не минимизирует ошибку. Затем должен быть некоторый набор значений, которые он округляет, который должен быть округлен в большую сторону, и наоборот (оба набора имеют одинаковый размер). Но каждое значение, которое оно округляет, дальше от следующего целого числа, чем любое значение, которое оно округляет (и vv), поэтому новая величина ошибки должна быть больше. QED. Однако это не работает для всех метрик ошибок; нужны другие алгоритмы.
Ричи

2

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

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%  |
-------------------------------------------------

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

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


это в значительной степени то, что я имел в виду, с той разницей, что ошибка должна измеряться относительно значения (округление от 9,8 до 10 - большая ошибка, чем округление от 19,8 до 20). Это можно легко сделать, отразив это в обратном вызове sort.
Поезд

это неверно для [33,33, 33,33, 33,33, 0,1], оно возвращает [1, 33, 33, 33], а не более точное [34, 33, 33, 0]
yonilevy

@yonilevy Спасибо за это. Исправлено сейчас.
Бруно

еще нет, для [16,666, 16,666, 16,666, 16,666, 16,666, 16,666] он возвращает [15, 17, 17, 17, 17, 17], а не [16, 16, 17, 17, 17, 17] - см. мой ответ
йонилевый

2

Я не уверен, какой уровень точности вам нужен, но я бы просто добавил 1 к первым nчислам, nявляющимся пределом общей суммы десятичных дробей. В данном случае это 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

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


1

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

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

Добавьте десятичные части. В вашем примере у вас есть общее количество дробной части = 3.

Ceil 3 числа с самыми высокими фракциями и пол остальных.

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


1
Хотя это может дать числа, которые прибавляются к 100, вы можете в конечном итоге превратить 3,9 в 3 и 25,1 в 26.
RobG

нет. 3.9 будет 4, а 25.1 - 25. Я сказал, что 3 числа с наибольшими долями, а не с самым высоким значением.
Арунлалам

2
если имеется слишком много дробей, заканчивающихся на 0,9, скажем, 9 значений 9,9% и одно значение 10,9, то есть одно значение, которое в итоге составит 9%, 8 - 10% и одно - 11%.
Арунлалам

1

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

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

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

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

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, реальное процентное значение для А увеличилось на 0,01%, но аппроксимация показывает уменьшение на 1%.

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

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


кто-нибудь знает, как сделать лучшие таблицы, пожалуйста, отредактируйте или скажите мне, как / где
Rolazaro Azeveires

0

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

скажем, число к;

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

0

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

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

0

Вот более простая реализация 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.


0

Для тех, у кого есть проценты в серии панд, вот моя реализация метода « Самый большой остаток» (как в ответе Варуна Вохры ), где вы можете даже выбрать десятичные дроби, до которых вы хотите округлить.

import numpy as np

def largestRemainderMethod(pd_series, decimals=1):

    floor_series = ((10**decimals * pd_series).astype(np.int)).apply(np.floor)
    diff = 100 * (10**decimals) - floor_series.sum().astype(np.int)
    series_decimals = pd_series - floor_series / (10**decimals)
    series_sorted_by_decimals = series_decimals.sort_values(ascending=False)

    for i in range(0, len(series_sorted_by_decimals)):
        if i < diff:
            series_sorted_by_decimals.iloc[[i]] = 1
        else:
            series_sorted_by_decimals.iloc[[i]] = 0

    out_series = ((floor_series + series_sorted_by_decimals) / (10**decimals)).sort_values(ascending=False)

    return out_series

-1

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


5
Это НЕ гарантирует, что округление уравновешивается - оно просто уменьшает количество ошибок, распределяя половину округления между четными и нечетными числами. Есть еще сценарии, когда округление банкиров дает неточные результаты.
D Стэнли

@DStanley Согласен. Я не сказал иначе. Я изложил свою цель . Очень осторожно.
маркиз Лорн

2
Справедливо, я неправильно истолковал то, что вы пытались сказать. В любом случае я не думаю, что это решит проблему, так как использование округления банкиров не изменит результаты в примере.
D Стэнли
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.