Cómo hacer que los porcentajes redondeados sumen 100%

192

Considere los cuatro porcentajes a continuación, representados como floatnúmeros:

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

Necesito representar estos porcentajes como números enteros. Si simplemente lo uso Math.round(), termino con un total de 101%.

14 + 48 + 10 + 29 = 101

Si lo uso parseInt(), termino con un total de 97%.

13 + 47 + 9 + 28 = 97

¿Qué es un buen algoritmo para representar cualquier número de porcentajes como números enteros mientras se mantiene un total del 100%?


Editar : después de leer algunos de los comentarios y respuestas, claramente hay muchas maneras de resolverlo.

En mi opinión, para permanecer fiel a los números, el resultado "correcto" es el que minimiza el error general, definido por la cantidad de error que se introduciría en relación con el 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)

En caso de empate (3.33, 3.33, 3.33) se puede tomar una decisión arbitraria (por ejemplo, 3, 4, 3).

poezn
fuente
21
Supongamos que tiene 3.33, 3.33 y 3.33. ¿Cuál harás 4?
RobG
3
Exactamente. La pregunta encarna una contradicción en los términos.
Marqués de Lorne
44
Es un escenario muy común en los informes: cómo mostrar un "total" de valores decimales que no siempre coincide con la suma de los valores mostrados.
D Stanley
1
¿Cuál es el "derecho" resultado en su caso de ejemplo? Eso puede resolver los desacuerdos sobre cuál es la "mejor" solución.
D Stanley

Respuestas:

35

Dado que ninguna de las respuestas aquí parece resolverlo correctamente, aquí está mi versión 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
fuente
66
Corrígeme si me equivoco, pero ¿no es esta una implementación del algoritmo propuesto por mi respuesta? (No se debe borrar en el subrayado)
vvohra87
@VarunVohra lo siento, no me di cuenta de esto hasta ahora, sí, parece que su algoritmo es el mismo :) no estoy seguro de por qué mi publicación es la respuesta aceptada, el código ofuscado fue solo para lolz ...
yonilevy
@yonilevy eliminó mi comentario; Simplemente no me di cuenta de que se suponía que debía devolver una lista ordenada. ¡Me disculpo!
Zack Burt
2
Hay un problema con esta función cuando el último elemento es 0 y los anteriores suman 100. Por ejemplo, [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. El último devuelve lógicamente -1. Pensé en la siguiente solución muy rápido, pero probablemente haya algo mejor: jsfiddle.net/0o75bw43/1
Cruclax
1
@Cruclax muestra todos los 1 cuando todas las entradas son cero en la matriz de entrada
tony.0919
158

Hay muchas maneras de hacer esto, siempre que no le preocupe la dependencia de los datos decimales originales.

El primer método y quizás el más popular sería el método de remanente más grande

Que es básicamente:

  1. Redondeando todo
  2. Obteniendo la diferencia en suma y 100
  3. Distribuir la diferencia sumando 1 a los elementos en orden decreciente de sus partes decimales

En su caso, sería así:

13.626332%
47.989636%
 9.596008%
28.788024%

Si tomas las partes enteras, obtienes

13
47
 9
28

que suma 97 y desea agregar tres más. Ahora nos fijamos en las partes decimales, que son

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

y tome los más grandes hasta que el total llegue a 100. Entonces obtendrá:

14
48
 9
29

Alternativamente, puede simplemente elegir mostrar un lugar decimal en lugar de valores enteros. Entonces, los números serían 48.3 y 23.9, etc. Esto reduciría mucho la varianza de 100.

vvohra87
fuente
55
Esta "columna de características" en el sitio web de la Sociedad Americana de Matemáticas - Distribución II: Sistemas de distribución - describe varios métodos similares de "distribución".
Kenny Evitt el
1
Esto casi parece una copia y pega de mi respuesta aquí stackoverflow.com/questions/5227215/… .
Sawa
Tenga en cuenta que, en contra de su comentario sobre la respuesta de @DStanley, en su respuesta el 9,596008% se redondeó al 9%, lo que representa una diferencia de más del 0,5%. Sin embargo, sigue siendo una buena respuesta.
Rolazaro Azeveires
32

Probablemente la "mejor" forma de hacer esto (citado ya que "mejor" es un término subjetivo) es mantener una cuenta corriente (no integral) de dónde se encuentra, y redondear eso valor.

Luego, utilícelo junto con el historial para determinar qué valor debe usarse. Por ejemplo, usando los valores que dio:

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

En cada etapa, no redondeas el número en sí. En cambio, redondeas el acumulado valor y calcula el mejor entero que alcanza ese valor desde la línea de base anterior: esa línea de base es el valor acumulado (redondeado) de la fila anterior.

Esto funciona porque estás no perder información en cada etapa, sino más bien el uso de la información de forma más inteligente. Los valores redondeados 'correctos' están en la columna final y puede ver que suman 100.

Puede ver la diferencia entre esto y redondear ciegamente cada valor, en el tercer valor anterior. Si bien 9.596008normalmente se redondearía a 10, el acumulado 71.211976se redondea correctamente a 71- esto significa que solo 9es necesario agregar a la línea de base anterior de 62.


Esto también funciona para una secuencia "problemática" como tres valores aproximados , donde uno1/3 de ellos debe redondearse:

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
fuente
1
El segundo enfoque soluciona ambos problemas. El primero da 26, 25, 26, 23, el segundo 1, 0, 1, 0, 1, 0, ....
paxdiablo
Este método también funciona bien para redondear números pequeños, ya que evita el pecado número negativo la salida
Jonty5817
18

El objetivo del redondeo es generar la menor cantidad de error. Cuando redondeas un valor único, ese proceso es simple y directo y la mayoría de las personas lo entienden fácilmente. Cuando redondea varios números al mismo tiempo, el proceso se vuelve más complicado: debe definir cómo se combinarán los errores, es decir, qué se debe minimizar.

La respuesta bien votada por Varun Vohra minimiza la suma de los errores absolutos, y es muy simple de implementar. Sin embargo, hay casos extremos que no maneja: cuál debería ser el resultado del redondeo24.25, 23.25, 27.25, 25.25 ? Uno de esos debe redondearse hacia arriba en lugar de hacia abajo. Probablemente elegiría arbitrariamente el primero o el último de la lista.

Quizás sea mejor usar el error relativo en lugar del absoluto error . Redondeando 23.25 hasta 24 lo cambia en un 3.2% mientras que redondeando 27.25 hasta 28 solo lo cambia en un 2.8%. Ahora hay un claro ganador.

Es posible ajustar esto aún más. Una técnica común es cuadrar cada error, para que los errores grandes cuenten desproporcionadamente más que los pequeños. También usaría un divisor no lineal para obtener el error relativo: no parece correcto que un error al 1% sea 99 veces más importante que un error al 99%. En el siguiente código, he usado la raíz cuadrada.

El algoritmo completo es el siguiente:

  1. Suma los porcentajes después de redondearlos todos hacia abajo y resta de 100. Esto te indica cuántos de esos porcentajes deben redondearse en su lugar.
  2. Genere dos puntajes de error para cada porcentaje, uno cuando se redondea hacia abajo y otro cuando se redondea hacia arriba. Toma la diferencia entre los dos.
  3. Ordene las diferencias de error producidas anteriormente.
  4. Para el número de porcentajes que deben redondearse, tome un elemento de la lista ordenada e incremente el porcentaje redondeado hacia abajo en 1.

Es posible que aún tenga más de una combinación con la misma suma de errores, por ejemplo 33.3333333, 33.3333333, 33.3333333 . Esto es inevitable, y el resultado será completamente arbitrario. El código que doy a continuación prefiere redondear los valores a la izquierda.

Poner todo junto en Python se ve así.

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 puede ver con el último ejemplo, este algoritmo aún es capaz de entregar resultados no intuitivos. A pesar de que 89.0 no necesita redondeo alguno, uno de los valores en esa lista necesitaba ser redondeado; el error relativo más bajo resulta de redondear ese gran valor en lugar de las alternativas mucho más pequeñas.

Esta respuesta originalmente abogó por pasar por todas las combinaciones posibles de redondeo arriba / abajo, pero como se señaló en los comentarios, un método más simple funciona mejor. El algoritmo y el código reflejan esa simplificación.

Mark Ransom
fuente
1
No creo que deba considerar todas las combinaciones: proceso en orden de disminución decreciente de error ponderado que va de redondeo a cero a redondeo al infinito (más o menos simplemente introduciendo el pesaje en las respuestas de Verun Vohras y yonilevy ("idénticas").
barba gris
@greybeard tienes razón, estaba pensando demasiado en esto. No pude ordenar el error ya que hay dos errores para cada valor, pero tomar la diferencia resolvió ese problema. He actualizado la respuesta.
Mark Ransom
Prefiero tener siempre 0% cuando el número real es 0%. Por lo tanto, agregar if actual == 0: return 0a error_genfunciona muy bien.
Nikolay Baluk
1
¿Cuál es el isclosemétodo al principio de round_to_100?
toto_tico
2
@toto_tico stackoverflow.com/questions/5595425/…
Mark Ransom
7

NO sume los números redondeados. Vas a tener resultados inexactos. El total podría estar significativamente apagado dependiendo del número de términos y la distribución de partes fraccionarias.

Muestra los números redondeados pero suma los valores reales. Dependiendo de cómo esté presentando los números, la forma real de hacerlo podría variar. De esa manera obtienes

 14
 48
 10
 29
 __
100

De cualquier manera que vaya, tendrá discrepancias. No hay forma en su ejemplo de mostrar números que sumen 100 sin "redondear" un valor de la manera incorrecta (el mínimo error sería cambiar 9.596 a 9)

EDITAR

Debe elegir entre uno de los siguientes:

  1. Precisión de los artículos
  2. Precisión de la suma (si está sumando valores redondeados)
  3. Consistencia entre los elementos redondeados y la suma redondeada)

La mayoría de las veces, cuando se trata de porcentajes n. ° 3, es la mejor opción porque es más obvio cuando el total es igual al 101% que cuando los elementos individuales no suman 100, y usted mantiene los elementos individuales con precisión. "Redondear" 9.596 a 9 es inexacto en mi opinión.

Para explicar esto, a veces agrego una nota al pie de página que explica que los valores individuales se redondean y pueden no sumar el 100%; cualquiera que entienda el redondeo debería poder entender esa explicación.

D Stanley
fuente
66
Eso no es muy útil ya que los valores impresos no suman 100. El propósito de la pregunta era evitar que los usuarios pensaran que los valores son incorrectos, lo que en este caso, la mayoría de las personas haría al mirar y comparar con el total .
vvohra87
@VarunVohra leyó mi edición, NO PUEDE mostrar sus números de modo que sumen 100 sin "redondear" uno en más de 0.5.
D Stanley
1
@DStanley en realidad, salvo un conjunto donde todos los números son menos de 0.5, puedes. Comprueba mi respuesta: LRM hace exactamente eso.
vvohra87
3
@VarunVohra En el ejemplo original, LRM arrojará 14, 48, 9 y 29 que "redondearán" 9.596 a 9. Si estamos asignando en base a números enteros, LRM será el más preciso, pero aún está cambiando un resultado por más de media unidad.
D Stanley
7

Escribí un asistente de redondeo de la versión C #, el algoritmo es el mismo que la respuesta de Varun Vohra , espero que ayude.

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

Pasa la siguiente prueba de Unidad:

[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
fuente
¡Agradable! me dio una base para empezar ... Enumerable no tiene ForEach aunque creo
Jack0fshad0ws
4

Puede intentar realizar un seguimiento de su error debido al redondeo y luego redondear contra el grano si el error acumulado es mayor que la parte fraccionaria del número actual.

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

No estoy seguro de si esto funcionaría en general, pero parece funcionar de manera similar si se invierte el orden:

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

Estoy seguro de que hay casos extremos en los que esto podría romperse, pero cualquier enfoque será al menos algo arbitrario, ya que básicamente está modificando sus datos de entrada.

atkretsch
fuente
2
Los contadores y banqueros han estado utilizando una técnica similar durante cientos de años. "Llevar el resto" de una fila a la siguiente. Comience con 1/2 de un centavo en el "llevar". Agregue el "carry" al primer valor y trunca. Ahora, la cantidad que perdiste truncando, pon eso en el "carry". Haga esto completamente hacia abajo, y los números redondeados se sumarán al total deseado exactamente cada vez.
Jeff Grigg el
Carolyn Kay sugirió esta implementación en Access VB 2007: <code> 'Dólares de reembolso redondos utilizando el método "carry the restder" ref1 = rsQry! [Refund Paid $$$] * rsQry! [Property Value] / propValTot ref2 = ref1 + ref5 'Agregue el resto llevado, cero para comenzar ref3 = ref2 * 100' Multiplique por 100 en un número entero ref4 = ref3 / 100 'Divida por 100 en un número decimal rsTbl! [Reembolso pagado $$$] = ref4' Ponga el " resto "número redondeado en la tabla ref5 = ref2 - ref4 'Lleve el nuevo resto </code>
Jeff Grigg
2

Una vez escribí una herramienta no redondeada, para encontrar la perturbación mínima de un conjunto de números para que coincida con un objetivo. Era un problema diferente, pero uno podría en teoría usar una idea similar aquí. En este caso, tenemos un conjunto de opciones.

Por lo tanto, para el primer elemento, podemos redondearlo a 14 o a 13. El costo (en un sentido de programación de enteros binarios) de hacerlo es menor para el redondeo hacia arriba que hacia abajo, porque el redondeo hacia abajo requiere que mover ese valor a una distancia mayor. Del mismo modo, podemos redondear cada número hacia arriba o hacia abajo, por lo que hay un total de 16 opciones entre las que debemos elegir.

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

Normalmente resolvería el problema general en MATLAB, aquí usando bintprog, una herramienta de programación de enteros binarios, pero solo hay algunas opciones para probar, por lo que es bastante fácil con bucles simples probar cada una de las 16 alternativas. Por ejemplo, supongamos que redondeamos este 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

El error absoluto total realizado es 1.25266. Se puede reducir ligeramente mediante el siguiente redondeo 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 hecho, esta será la solución óptima en términos del error absoluto. Por supuesto, si hubiera 20 términos, el espacio de búsqueda será de tamaño 2 ^ 20 = 1048576. Para 30 o 40 términos, ese espacio será de tamaño significativo. En ese caso, necesitaría usar una herramienta que pueda buscar eficientemente el espacio, tal vez usando un esquema de ramificación y enlace.


fuente
Solo para referencia futura: el algoritmo del "resto más grande" debe minimizar el error absoluto total de acuerdo con su métrica (consulte la respuesta de @ varunvohra). La prueba es simple: suponga que no minimiza el error. Entonces debe haber algún conjunto de valores que se redondea hacia abajo que se debe redondear hacia arriba, y viceversa (los dos conjuntos son del mismo tamaño). Pero cada valor que redondea hacia abajo está más alejado del siguiente entero que cualquier valor que redondea (y vv), por lo que la nueva cantidad de error debe ser mayor. QED Sin embargo, no funciona para todas las métricas de error; Se necesitan otros algoritmos.
rici
2

Creo que lo siguiente logrará lo que buscas

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]

Una última cosa, ejecuté la función usando los números originalmente dados en la pregunta para comparar con la salida deseada

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

Esto era diferente a lo que la pregunta quería => [48, 29, 14, 9]. No pude entender esto hasta que miré el margen de error total

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

Esencialmente, el resultado de mi función en realidad introduce la menor cantidad de error.

Violín aquí

Bruno
fuente
eso es más o menos lo que tenía en mente, con la diferencia de que el error debe medirse en relación con el valor (redondear 9.8 a 10 es un error mayor que redondear de 19.8 a 20). Sin embargo, esto podría hacerse fácilmente reflejándolo en la devolución de llamada de clasificación.
poezn
esto está mal para [33.33, 33.33, 33.33, 0.1], devuelve [1, 33, 33, 33] en lugar de la más precisa [34, 33, 33, 0]
yonilevy
@yonilevy Gracias por eso. Corregido ahora.
Bruno
todavía no, para [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] devuelve [15, 17, 17, 17, 17, 17] en lugar de [16, 16, 17, 17, 17, 17] - mira mi respuesta
yonilevy
2

No estoy seguro de qué nivel de precisión necesita, pero lo que haría es simplemente agregar 1 a los primeros nnúmeros, que nes el límite máximo de la suma total de decimales. En este caso 3, es decir , agregaría 1 a los primeros 3 elementos y colocaría el resto. Por supuesto, esto no es súper preciso, algunos números pueden redondearse hacia arriba o hacia abajo cuando no debería, pero funciona bien y siempre dará como resultado el 100%.

Entonces [ 13.626332, 47.989636, 9.596008, 28.788024 ]sería [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

Siempre puede informar a los usuarios que los números son redondeados y pueden no ser muy precisos ...

elclanrs
fuente
1

Si lo está redondeando, no hay una buena manera de hacerlo exactamente igual en todos los casos.

Puedes tomar la parte decimal de los N porcentajes que tienes (en el ejemplo que diste es 4).

Suma las partes decimales. En su ejemplo, tiene un total de parte fraccionaria = 3.

Coloca el techo en los 3 números con las fracciones más altas y coloca el resto.

(Perdón por las ediciones)

arunlalam
fuente
1
Si bien eso puede proporcionar números que sumen 100, puede terminar convirtiendo 3.9 en 3 y 25.1 en 26.
RobG
No. 3.9 será 4 y 25.1 será 25. Dije que los 3 números con fracciones más altas no sean el valor más alto.
Arunlalam
2
si hay demasiadas fracciones que terminan en .9, digamos 9 valores de 9.9% y un valor de 10.9, hay un valor que terminará en 9%, 8 como 10% y uno como 11%.
Arunlalam
1

Si realmente debe redondearlos, ya hay muy buenas sugerencias aquí (resto más grande, menos error relativo, etc.).

También hay una buena razón para no redondear (obtendrá al menos un número que "se ve mejor" pero está "equivocado") y cómo resolverlo (advertir a sus lectores) y eso es lo que hago.

Permítanme agregar la parte del número "incorrecta".

Suponga que tiene tres eventos / entidades / ... con algunos porcentajes que aproxima como:

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

Más adelante los valores cambian ligeramente, a

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

La primera tabla tiene el problema ya mencionado de tener un número "incorrecto": 33.34 está más cerca de 33 que de 34.

Pero ahora tienes un error más grande. Comparando el día 2 con el día 1, el valor de porcentaje real para A aumentó, en un 0.01%, pero la aproximación muestra una disminución en un 1%.

Ese es un error cualitativo, probablemente bastante peor que el error cuantitativo inicial.

Se podría idear una aproximación para todo el conjunto, pero es posible que tenga que publicar datos el primer día, por lo que no sabrá sobre el segundo día. Entonces, a menos que realmente, realmente, deba aproximarse, probablemente sea mejor que no.

Rolazaro Azeveires
fuente
cualquiera que sepa cómo hacer mejores tablas, edite o dígame cómo / dónde
Rolazaro Azeveires
0

compruebe si esto es válido o no, en lo que respecta a mis casos de prueba, puedo hacer que esto funcione.

digamos que número es k;

  1. ordenar porcentaje por orden descendente.
  2. iterar sobre cada porcentaje de orden descendente.
  3. calcule el porcentaje de k para el primer porcentaje tome Math.Ceil de salida.
  4. siguiente k = k-1
  5. iterar hasta que se consuma todo el porcentaje.
flojo
fuente
0

He implementado el método de la respuesta de Varun Vohra aquí para listas y dictados.

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
fuente
0

Aquí hay una implementación Python más simple de la respuesta @ 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

Es necesario math, itertools, operator.

CMCDragonkai
fuente
0

Para aquellos que tienen los porcentajes en una serie de pandas, aquí está mi implementación del método de resto más grande (como en la respuesta de Varun Vohra ), donde incluso puede seleccionar los decimales a los que desea redondear.

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
fuente
-1

Este es un caso para el redondeo de los banqueros, también conocido como 'redondo medio par'. Es compatible con BigDecimal. Su propósito es asegurar que el redondeo se equilibre, es decir, no favorezca ni al banco ni al cliente.

Marqués de Lorne
fuente
55
NO garantiza que el redondeo se equilibre, solo reduce la cantidad de error al distribuir el redondeo entre números pares e impares. Todavía hay escenarios en los que el redondeo de los banqueros produce resultados inexactos.
D Stanley
@DStanley estuvo de acuerdo. No dije lo contrario. Dije su propósito . Muy cuidadosamente.
Marqués de Lorne el
2
Es justo: malinterpreté lo que intentabas decir. En cualquier caso, no creo que resuelva el problema, ya que el uso de redondeo bancario no cambiará los resultados en el ejemplo.
D Stanley