Como fazer porcentagens arredondadas somar 100%

192

Considere as quatro porcentagens abaixo, representadas como floatnúmeros:

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

Eu preciso representar essas porcentagens como números inteiros. Se eu simplesmente usar Math.round(), acabarei com um total de 101%.

14 + 48 + 10 + 29 = 101

Se eu usar parseInt(), acabarei com um total de 97%.

13 + 47 + 9 + 28 = 97

O que é um bom algoritmo para representar qualquer número de porcentagens como números inteiros, mantendo um total de 100%?


Edit : Depois de ler alguns dos comentários e respostas, há claramente muitas maneiras de resolver isso.

Na minha opinião, para permanecer fiel aos números, o resultado "correto" é o que minimiza o erro geral, definido por quanto arredondamento de erro seria introduzido em relação ao valor real:

        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)

Em caso de empate (3.33, 3.33, 3.33), uma decisão arbitrária pode ser tomada (por exemplo, 3, 4, 3).

poezn
fonte
21
Suponha que você tenha 3.33, 3.33 e 3.33. Qual você fará 4?
RobG
3
Exatamente. A questão incorpora uma contradição em termos.
Marquês de Lorne
4
É um cenário muito comum nos relatórios - como exibir um "total" de valores decimais que nem sempre corresponde à soma dos valores exibidos.
D # Stanley #
1
Qual é o resultado "certo" no seu caso de exemplo? Isso pode resolver as divergências sobre qual é a "melhor" solução.
D # Stanley #

Respostas:

35

Como nenhuma das respostas aqui parece resolvê-lo corretamente, aqui está minha versão semi-ofuscada usando 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]
yonilevy
fonte
6
Corrija-me se estiver errado, mas isso não é uma implementação do algoritmo proposto pela minha resposta? (Não limpar em underscorejs)
vvohra87 2/12/12
@VarunVohra desculpe, eu não percebi isso até agora, sim, parece que seu algoritmo é o mesmo :) não sei por que minha postagem é a resposta aceita, o código ofuscado era apenas para o lolz ...
yonilevy
@yonilevy Excluiu meu comentário; Só não percebi que deveria retornar uma lista classificada. Peço desculpas!
Zack Burt
2
Há um problema com esta função quando o último elemento é 0 e os anteriores são adicionados a 100. Por exemplo, [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. O último retorna logicamente -1. Pensei na solução a seguir muito rapidamente, mas provavelmente há algo melhor: jsfiddle.net/0o75bw43/1
Cruclax
1
@Cruclax mostra todos os 1 quando todas as entradas são zero na matriz de entrada
tony
158

Há várias maneiras de fazer exatamente isso, desde que você não esteja preocupado com a confiança nos dados decimais originais.

O primeiro e talvez o mais popular método seria o Maior Método Restante

O que é basicamente:

  1. Arredondando tudo para baixo
  2. Obtendo a diferença na soma e 100
  3. Distribuindo a diferença adicionando 1 aos itens em ordem decrescente de suas casas decimais

No seu caso, seria assim:

13.626332%
47.989636%
 9.596008%
28.788024%

Se você pegar as partes inteiras, obtém

13
47
 9
28

que soma 97, e você deseja adicionar mais três. Agora, você olha as partes decimais, que são

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

e pegue as maiores até o total chegar a 100. Então você obteria:

14
48
 9
29

Como alternativa, você pode simplesmente optar por mostrar uma casa decimal em vez de valores inteiros. Portanto, os números seriam 48,3 e 23,9 etc. Isso reduziria muito a variação de 100.

vvohra87
fonte
5
Esta "Coluna de Recursos" no site da American Mathematics Society - Repartição II: Sistemas de Repartição - descreve vários métodos semelhantes de "repartição".
precisa saber é o seguinte
1
Isso quase se parece com uma cópia e colar da minha resposta aqui stackoverflow.com/questions/5227215/… .
sawa
Observe que, ao contrário do seu comentário na resposta de @DStanley, na sua resposta 9,596008% foi arredondado para 9%, o que é mais do que uma diferença de 0,5%. Ainda é uma boa resposta, no entanto.
Rolazaro Azeveires 16/10/19
32

Provavelmente, a "melhor" maneira de fazer isso (citada como "melhor" é um termo subjetivo) é manter um registro contínuo (não integral) de onde você está e contorná- lo valor.

Em seguida, use isso junto com o histórico para descobrir qual valor deve ser usado. Por exemplo, usando os valores que você forneceu:

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

Em cada estágio, você não arredonda o número em si. Em vez disso, você arredonda o valor acumulado valor e calcula o melhor número inteiro que atinge esse valor a partir da linha de base anterior - essa linha de base é o valor acumulado (arredondado) da linha anterior.

Isso funciona porque você não está perdendo informações em cada estágio, mas usando as informações de maneira mais inteligente. Os valores arredondados 'corretos' estão na coluna final e você pode ver que eles somam 100.

Você pode ver a diferença entre isso e arredondar cegamente cada valor, no terceiro valor acima. Embora 9.596008normalmente seja arredondado para 10, o acumulado é 71.211976arredondado corretamente para 71- isso significa que apenas 9é necessário adicionar à linha de base anterior de 62.


Isso também funciona para a sequência "problemática", como três valores aproximados , em que um deles deve ser arredondado: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
paxdiablo
fonte
1
A segunda abordagem corrige esses dois problemas. O primeiro dá 26, 25, 26, 23, o segundo 1, 0, 1, 0, 1, 0, ....
precisa
Esta abordagem também funciona bem para arredondar números pequenos em que evita número pecado negativo da saída
Jonty5817
18

O objetivo do arredondamento é gerar a menor quantidade de erros. Quando você arredonda um único valor, esse processo é simples e direto, e a maioria das pessoas o entende facilmente. Ao arredondar vários números ao mesmo tempo, o processo fica mais complicado - você deve definir como os erros serão combinados, ou seja, o que deve ser minimizado.

A resposta bem votada de Varun Vohra minimiza a soma dos erros absolutos e é muito simples de implementar. No entanto, existem casos extremos que ele não lida - qual deve ser o resultado do arredondamento 24.25, 23.25, 27.25, 25.25? Um deles precisa ser arredondado para cima em vez de para baixo. Você provavelmente escolheria arbitrariamente o primeiro ou o último da lista.

Talvez seja melhor usar o erro relativo em vez do absoluto erro . O arredondamento de 23,25 a 24 altera em 3,2%, enquanto o de 27,25 a 28 altera apenas 2,8%. Agora há um vencedor claro.

É possível ajustar isso ainda mais. Uma técnica comum é ajustar cada erro ao quadrado , de modo que os erros maiores sejam desproporcionalmente mais que os pequenos. Eu também usaria um divisor não linear para obter o erro relativo - não parece certo que um erro de 1% seja 99 vezes mais importante do que um erro de 99%. No código abaixo, usei a raiz quadrada.

O algoritmo completo é o seguinte:

  1. Soma as porcentagens após arredondá-las para baixo e subtrai de 100. Isso indica quantas dessas porcentagens devem ser arredondadas para cima.
  2. Gere duas pontuações de erro para cada porcentagem, uma quando arredondada para baixo e outra quando arredondada para cima. Pegue a diferença entre os dois.
  3. Classifique as diferenças de erro produzidas acima.
  4. Para o número de porcentagens que precisam ser arredondadas, pegue um item da lista classificada e aumente a porcentagem arredondada em 1.

Você ainda pode ter mais de uma combinação com a mesma soma de erros, por exemplo 33.3333333, 33.3333333, 33.3333333 . Isso é inevitável, e o resultado será completamente arbitrário. O código que eu dou abaixo prefere arredondar os valores à esquerda.

Juntar tudo isso em Python se parece com isso.

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]

Como você pode ver no último exemplo, esse algoritmo ainda é capaz de fornecer resultados não intuitivos. Embora 89.0 não precise de arredondamento, um dos valores nessa lista precisava ser arredondado; o menor erro relativo resulta do arredondamento desse valor grande em vez das alternativas muito menores.

Essa resposta originalmente defendia todas as combinações possíveis de arredondamento para cima / baixo, mas, como indicado nos comentários, um método mais simples funciona melhor. O algoritmo e o código refletem essa simplificação.

Mark Ransom
fonte
1
Eu não acho que você precise considerar todas as combinações: processo em ordem decrescente de queda no erro ponderado, passando de zero a zero para arredondar até o infinito (praticamente introduzindo a ponderação nas respostas de Verun Vohras e yonilevy ("idênticas")).
23616
@greybeard você está certo, eu estava pensando demais nisso. Não pude apenas classificar o erro, pois existem dois erros para cada valor, mas a diferença resolveu esse problema. Eu atualizei a resposta.
Mark Ransom
Prefiro sempre ter 0% quando o número real é 0%. Então, adicionando if actual == 0: return 0a error_genfunciona muito bem.
Nikolay Baluk
1
qual é o isclosemétodo no início de round_to_100?
toto_tico
2
@toto_tico stackoverflow.com/questions/5595425/…
Mark Ransom
7

NÃO somar os números arredondados. Você terá resultados imprecisos. O total pode ser reduzido significativamente, dependendo do número de termos e da distribuição de partes fracionárias.

Exiba os números arredondados, mas some os valores reais. Dependendo de como você está apresentando os números, a maneira real de fazer isso varia. Dessa forma, você obtém

 14
 48.
 10
 29
 __
100

De qualquer maneira, você terá discrepância. No seu exemplo, não há como mostrar números que somam 100 sem "arredondar" um valor da maneira errada (o menor erro seria alterar 9,596 para 9)

EDITAR

Você precisa escolher entre um dos seguintes:

  1. Precisão dos itens
  2. Precisão da soma (se você estiver somando valores arredondados)
  3. Consistência entre os itens arredondados e a soma arredondada)

Na maioria das vezes, ao lidar com as porcentagens nº 3, é a melhor opção, porque é mais óbvio quando o total é igual a 101% do que quando os itens individuais não totalizam 100 e você mantém os itens individuais precisos. "Arredondar" 9.596 a 9 é impreciso na minha opinião.

Para explicar isso, às vezes adiciono uma nota de rodapé que explica que os valores individuais são arredondados e podem não totalizar 100% - qualquer pessoa que entenda o arredondamento deve entender essa explicação.

D Stanley
fonte
6
Isso não é muito útil, pois os valores impressos não somam 100. O objetivo da pergunta era impedir que os usuários pensassem que os valores estavam incorretos, o que, nesse caso, a maioria das pessoas faria ao olhar e comparar com o total .
vvohra87
@VarunVohra leu minha edição, você NÃO PODE exibir seus números de forma que eles totalizem até 100 sem "arredondar" um por mais de 0,5.
D # Stanley #
1
@ DStanley, na verdade, exceto um conjunto em que todos os números são tímidos de 0,5, você pode. Verifique minha resposta - o LRM faz exatamente isso.
vvohra87
3
@VarunVohra No exemplo original, o LRM produzirá 14, 48, 9 e 29, que "arredondarão" 9.596 para 9. Se estiver alocando com base em números inteiros, o LRM será o mais preciso, mas ainda está alterando um resultado por mais que meia unidade.
D # Stanley #
7

Eu escrevi um auxiliar de arredondamento de versão em C #, o algoritmo é o mesmo que a resposta de Varun Vohra , espero que ajude.

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;
}

Passa no seguinte teste de unidade:

[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 });
}
Bruce
fonte
Agradável! me deu uma base de chão para começar com .. não Enumerable não tem ForEach embora eu acredite
Jack0fshad0ws
4

Você pode tentar acompanhar o seu erro devido ao arredondamento e, em seguida, arredondar na granulação se o erro acumulado for maior que a parte fracionária do número atual.

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

Não tenho certeza se isso funcionaria em geral, mas parece funcionar semelhante se a ordem for revertida:

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

Tenho certeza de que existem casos extremos onde isso pode ser interrompido, mas qualquer abordagem será pelo menos um pouco arbitrária, pois você está basicamente modificando seus dados de entrada.

atkretsch
fonte
2
Contadores e banqueiros usam uma técnica semelhante há centenas de anos. "Carregar o restante" de uma linha para a seguinte. Comece com 1/2 de um centavo no "carry". Adicione o "carry" ao primeiro valor e trunque. Agora, o valor que você perdeu ao truncar, coloque isso no "carry". Faça isso até o fim, e os números arredondados serão adicionados sempre ao total desejado.
18716 Jeff Grigg #
Carolyn Kay sugeriu esta implementação no Access VB 2007: <code> 'Arredondar os dólares de reembolso usando o método "carry the restante" ref1 = rsQry! [Reembolso pago $$$] * rsQry! [Valor da propriedade] / propValTot ref2 = ref1 + ref5 'Adicione o restante transportado, zero para iniciar ref3 = ref2 * 100' Multiplique por 100 em um número inteiro ref4 = ref3 / 100 'Divida por 100 em um número decimal rsTbl! [Reembolso pago $$$] = ref4' Coloque o " restante "número arredondado na tabela ref5 = ref2 - ref4 'Leve o novo restante </code>
Jeff Grigg
2

Certa vez, escrevi uma ferramenta não-arredondada, para encontrar a perturbação mínima em um conjunto de números para corresponder a um objetivo. Era um problema diferente, mas em teoria se poderia usar uma idéia semelhante aqui. Nesse caso, temos um conjunto de opções.

Assim, para o primeiro elemento, podemos arredondá-lo para 14 ou para 13. O custo (no sentido de programação de número inteiro binário) de fazer isso é menor para o arredondamento para cima do que para o arredondamento para baixo, porque o arredondamento para baixo exige que mova esse valor para uma distância maior. Da mesma forma, podemos arredondar cada número para cima ou para baixo, para que haja um total de 16 opções que devemos escolher.

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

Normalmente, eu resolveria o problema geral no MATLAB, aqui usando o bintprog, uma ferramenta de programação de números inteiros binários, mas existem apenas algumas opções a serem testadas, por isso é fácil o suficiente com loops simples para testar cada uma das 16 alternativas. Por exemplo, suponha que arredondássemos esse conjunto como:

 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

O erro absoluto total cometido é 1.25266. Pode ser reduzido levemente pelo seguinte arredondamento alternativo:

 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

De fato, esta será a solução ideal em termos de erro absoluto. Obviamente, se houver 20 termos, o espaço de pesquisa terá o tamanho 2 ^ 20 = 1048576. Para 30 ou 40 termos, esse espaço terá um tamanho significativo. Nesse caso, você precisaria usar uma ferramenta que possa pesquisar com eficiência o espaço, talvez usando um esquema de ramificação e de ligação.


fonte
Apenas para referência futura: o algoritmo "maior restante" deve minimizar o erro absoluto total de acordo com sua métrica (consulte a resposta de @ varunvohra). A prova é simples: suponha que não minimize o erro. Então deve haver algum conjunto de valores que ele arredonde para baixo, que deve ser arredondado para cima e vice-versa (os dois conjuntos são do mesmo tamanho). Porém, todo valor arredondado está mais distante do próximo número inteiro do que qualquer valor arredondado (e vv), portanto a nova quantidade de erro deve ser maior. QED. No entanto, ele não funciona para todas as métricas de erro; outros algoritmos são necessários.
rici
2

Eu acho que o seguinte alcançará o que você procura

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]

Uma última coisa, executei a função usando os números originalmente fornecidos na pergunta para comparar com a saída desejada

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

Isso era diferente do que a pergunta queria => [48, 29, 14, 9]. Eu não conseguia entender isso até olhar para a margem total de erro

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

Essencialmente, o resultado da minha função realmente introduz a menor quantidade de erro.

Violino aqui

Bruno
fonte
isso é praticamente o que eu tinha em mente, com a diferença de que o erro deve ser medido em relação ao valor (arredondar 9,8 a 10 é um erro maior do que arredondar 19,8 a 20). Isso poderia ser feito facilmente, refletindo-o no retorno de chamada de classificação.
poezn
isso está errado para [33.33, 33.33, 33.33, 0.1], retorna [1, 33, 33, 33] em vez do mais preciso [34, 33, 33, 0]
yonilevy
@yonilevy Obrigado por isso. Corrigido agora.
Bruno
ainda não, para [16,666, 16,666, 16,666, 16,666, 16,666, 16,666] ele retorna [15, 17, 17, 17, 17, 17] em vez de [16, 16, 17, 17, 17, 17] - veja meu resposta
yonilevy
2

Não tenho certeza de qual nível de precisão você precisa, mas o que eu faria é simplesmente adicionar 1 os primeiros nnúmeros, nsendo o teto da soma total de casas decimais. Nesse caso 3, eu adicionaria 1 aos 3 primeiros itens e o restante do piso. É claro que isso não é super preciso, alguns números podem ser arredondados para cima ou para baixo quando não devem, mas funcionam bem e sempre resultam em 100%.

Então [ 13.626332, 47.989636, 9.596008, 28.788024 ]seria [14, 48, 10, 28]porqueMath.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

Você sempre pode informar aos usuários que os números são arredondados e podem não ser precisos ...

elclanrs
fonte
1

Se você estiver arredondando, não há uma boa maneira de obtê-lo exatamente da mesma forma em todos os casos.

Você pode pegar a parte decimal das porcentagens de N que você possui (no exemplo que você deu é 4).

Adicione as partes decimais. No seu exemplo, você tem um total de parte fracionária = 3.

Limite os 3 números com as frações mais altas e coloque o restante no chão.

(Desculpe pelas edições)

arunlalam
fonte
1
Enquanto que pode fornecer números que adicionam a 100, você pode acabar virando 3.9 em 3 e 25,1 em 26.
RobG
não. 3,9 será 4 e 25,1 será 25. Eu disse para tecer os 3 números com frações mais altas e não com o valor mais alto.
arunlalam
2
se houver frações demais terminando em 0,9, diga 9 valores de 9,9% e um valor de 10,9, um valor que terminará em 9%, 8 em 10% e 1 em 11%.
arunlalam
1

Se você realmente precisa arredondá-los, já existem sugestões muito boas aqui (restante restante, menor erro relativo e assim por diante).

Também já existe um bom motivo para não arredondar (você obterá pelo menos um número que "parece melhor", mas "errado") e como resolver isso (avise seus leitores) e é isso que eu faço.

Deixe-me adicionar a parte do número "errado".

Suponha que você tenha três eventos / entidades / ... com algumas porcentagens aproximadas como:

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

Posteriormente, os valores mudam ligeiramente, para

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

A primeira tabela tem o problema já mencionado de ter um número "errado": 33,34 está mais próximo de 33 do que de 34.

Mas agora você tem um erro maior. Comparando o dia 2 ao dia 1, o valor percentual real de A aumentou 0,01%, mas a aproximação mostra uma diminuição de 1%.

Esse é um erro qualitativo, provavelmente muito pior que o erro quantitativo inicial.

Pode-se conceber uma aproximação para todo o conjunto, mas talvez você precise publicar dados no primeiro dia, portanto, não saberá sobre o segundo dia. Portanto, a menos que você realmente, realmente, precise se aproximar, é melhor que não seja.

Rolazaro Azeveires
fonte
alguém que sabe como fazer mesas melhores, por favor edite ou me diga como / onde #
Rolazaro Azeveires
0

verificar se isso é válido ou não, tanto quanto meus casos de teste, eu consigo fazer isso funcionar.

digamos que número é k;

  1. classifique a porcentagem por ordem decrescente.
  2. itere sobre cada porcentagem da ordem decrescente.
  3. calcular porcentagem de k para a primeira porcentagem, obter Math.Ceil da saída.
  4. próximo k = k-1
  5. itere até que toda a porcentagem seja consumida.
relaxado
fonte
0

Eu implementei o método da resposta de Varun Vohra aqui para listas e ditados.

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
beruic
fonte
0

Aqui está uma implementação mais simples do Python da resposta @ 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

Você precisa math, itertools, operator.

CMCDragonkai
fonte
0

Para aqueles que têm as porcentagens em uma série de pandas, aqui está a minha implementação do método Maior Remanescente (como na resposta de Varun Vohra ), onde você pode até selecionar os decimais para os quais deseja arredondar.

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
maxi.marufo
fonte
-1

Esse é o caso do arredondamento de banqueiros, também conhecido como 'round half-even'. É suportado pelo BigDecimal. Seu objetivo é garantir que o arredondamento seja equilibrado, ou seja, não favorece nem o banco nem o cliente.

Marquês de Lorne
fonte
5
NÃO garante que o arredondamento seja equilibrado - apenas reduz a quantidade de erro distribuindo o meio-arredondamento entre números pares e ímpares. Ainda existem cenários em que o arredondamento dos banqueiros produz resultados imprecisos.
D # Stanley #
@DStanley concordou. Eu não disse o contrário. Eu declarei seu propósito . Muito cuidado.
Marquês de Lorne
2
É justo - interpretei mal o que você estava tentando dizer. Em ambos os casos, não acho que isso resolva o problema, pois o uso do arredondamento de banqueiros não alterará os resultados no exemplo.
D # Stanley #