2010-08-31 15 views
5

Como el título describe, tengo un conjunto de objetos, llámalos Asignaciones - que contienen una descripción & un número. Todos los números en el conjunto suman hasta el 100%, pero para fines de visualización a veces redondeo a un porcentaje completo. En algunos casos extremos, al haber redondeado los números, terminé con el 99%.Encuentra el número más alto en un conjunto que se redondeará hacia abajo y en su lugar

Ejemplo:

Description | Actual | Rounded 
=============================== 
Allocation A | 65.23% | 65% 
Allocation B | 25.40% | 25% 
Allocation C | 7.95% | 8% 
Allocation D | 1.42% | 1% 
=============================== 
Total  | 100% | 99% (Bad!) 

La solución solicitada, que es imperfecta, pero va a hacer, es encontrar la más alta que se debe redondear hacia abajo, y alrededor de ella en su lugar. En el ejemplo anterior, 1.42% se convertiría en 2% cuando se redondea. Editar: Por "El más alto para ser redondeado hacia abajo" me refiero al que se redondea más lejos. De ahí que el 1,42% se redondea hacia abajo en 0,42 mientras que el 65,23 solamente está siendo redondea hacia abajo 0,23

Así que ahora el código, tengo una clase

public class Allocation 
{ 
    public string Description {get;set;} 
    public doubel Percentage {get;set;} 
} 

Y éstos se llevan a cabo en un IEnumerable<Allocation>. Entonces, potencialmente usando LINQ, ¿cómo puedo determinar cuál es el que debe redondear? O más específicamente, ¿cómo puedo generar un nuevo IEnumerable<Allocation> con los números redondeados?

Si alguien tiene alguna otra sugerencia para siempre hacer porcentaje redondeado siempre equivale a 100% que sería aún mejor!

+0

Un comentario sobre los requisitos y el algoritmo: parece que se desea determinar la cantidad de encima o por debajo de 100 sus valores redondeados son, a continuación, ajuste tantos valores como sea necesario por ellos redondeo hacia otro lado. Mientras lo hace, querrá introducir el mínimo error. Por lo tanto, para los datos que proporcionó, la Asignación B debería redondearse porque 42 es el valor fraccionario más alto que se redondeó hacia abajo. ¿Eso suena bien? –

+0

Ah, y permítanme agregar una cosa más: dado que estos son valores financieros mantenidos con dos decimales, debe usar 'decimal', no' doble'. –

+0

La respuesta de Supercat plantea la pregunta de cómo definimos el mínimo error. ¿Nos referimos a un error mínimo absoluto o un error relativo mínimo? –

Respuesta

3

Como ho1 ha indicado, la solución para agregar 1 a una fila específica no resuelve el problema real.

Considere estos escenarios:

3 items evenly divided, 100/3 = 33 ; 33 * 3 = 99 ; Error = -1 
7 items evenly divided, 100/7 = 14 ; 14 * 7 = 98 ; Error = -2 
66 items evenly divided, 100/66 = 2 ; 2 * 66 = 132 ; Error = 32 

Aquí hay un código no probado que podrían conseguir que cerca de donde tiene que ir. Probablemente haya un error de signo aquí, así que ten cuidado.

public class AllocationRoundingWrapper 
{ 
    public Allocation Original {get;set;} 
    public double Rounded {get;set;} 
    public double IntroducedError() 
    { 
    return Rounded - Original.Percentage; 
    } 
} 

    //project the Allocations into Wrappers for rounding efforts. 

List<Allocation> source = GetAllocations(); 

List<AllocationRoundingWrapper> roundingWrappers = source 
    .Select(a => new AllocationRoundingWrapper() 
    { 
    Original = a, 
    Rounded = Math.Round(a.Percentage) 
    }).ToList(); 

int error = (int) roundingWrappers.Sum(x => x.IntroducedError()); 

    //distribute the rounding errors across the 
    // items with the absolute largest error. 

List<RoundingWrapper> orderedItems = error > 0 ? 
    roundingWrappers.OrderByDescending(x => x.IntroducedError()).ToList() : 
    roundingWrappers.OrderBy(x => x.IntroducedError()).ToList(); 

IEnumerator<RoundingWrapper> enumerator = orderedItems.GetEnumerator(); 

while(error > 0) 
{ 
    enumerator.MoveNext(); 
    enumerator.Current.Rounded += 1.0; 
    error -= 1; 
} 
while(error < 0) 
{ 
    enumerator.MoveNext(); 
    enumerator.Current.Rounded -= 1.0; 
    error += 1; 
} 

    //project back into Allocations for the result 
List<Allocation> result = roundingWrappers 
    .Select(x => new Allocation() 
    { 
    Description = x.Original.Description, 
    Percentage = x.Rounded 
    }).ToList(); 

Nota: Hacer un pedido según el error introducido puede provocar lazos. Considere el caso de 3 elementos, solo un elemento obtendrá +1 ... puede esperar que ese artículo sea elegido de manera consistente. Si se esperan resultados consistentes de múltiples ejecuciones, los vínculos deberían romperse.

+0

+1, sin embargo, el OP señaló que los valores reales, @ 2dp están garantizados al 100%, por lo que sus escenarios son imposibles. –

+0

La peor es 67 medidas iguales: '67 artículos divididos en partes iguales, 100/67 = 1; 1 * 67 = 67; Error = -33' –

+0

@David B ~ mi mal, cuando dije +1, lo dije en serio ... simplemente retrasé :) lo siento. También 100/7 = ~ 14.2857 y 14.29 * 7 = 100.03 –

2

En cuanto a obtener el 100%, ¿por qué no ejecutar el cálculo original una vez primero y ver qué porcentaje obtiene y luego sabe cuántos necesita redondear vs hacia abajo viendo cuántos puntos porcentuales difiere de 100%.

Así que si terminas con el 97%, redondea 3 números en lugar de hacia abajo. O si termina con el 102%, redondee los dos números con los decimales más bajos (más de 0,5) hacia abajo en lugar de hacia arriba.

+0

Solo para aclarar, solo puede ser 99% o 100% - y determinar que tengo una situación de 99% no es un problema - es saber qué elemento del conjunto debe redondear en lugar de hacia abajo. – Jamiec

+0

@Jamiec: Es suficiente, pero solo por curiosidad, ¿estás seguro de que solo puede ser del 99% o del 100%? ¿Tiene alguna otra verificación para asegurarse de que los valores no podrían haber sido algo así como: 65.47, 25.51, 7.51 y 1.51? –

+2

De acuerdo con ho1. ¿Estás diciendo que solo puede ser 99 porque eso es todo lo que has encontrado o existe una regla de negocios que lo impida? Mire 50.50 + 49.50 para una caja 101. Mire 100%/16 para un caso donde redondeado = 100 y no redondeado = 96. –

1
var HighestDown = 
     allocation.Where(a=>Math.Round(a.Percentage) == Math.Floor(a.Percentage) 
       .Max(a=>a.Percentage - Math.Floor(a.Percentage)); 

    HighestDown.Percentage = Math.Ceiling(HighestDown.Percentage); 

    var roundedAllocations = for a in allocation 
          select new Allocation 
            { 
             Description = a.Description, 
             Percentage = Math.Round(a.Percentage) 
            }; 
+0

Gracias por responder, parecía prometedor y similar a lo que estoy intentando a nivel local, excepto que obtiene el número más alto, en lugar del que se redondea más lejos. Ver mi edición para el OP. – Jamiec

+0

@Jamiec: Ver mi edición de mi respuesta (PS: Crecí siendo "Jamie C") –

+0

Curren - Mi apellido es Cohen. Mi verdadero nombre es James. No puedo decirte cuántas veces se escucha mi nombre como "James Curran" en el teléfono. Spoooky! – Jamiec

3

Sugiero siempre redondear hacia abajo, y luego si el resultado es 100-n, redondee los números con los 'n' residuales más grandes. Eso funcionará para cualquier información. Los enfoques que redondean al más cercano y luego intentan ajustar el resultado tienden a ser más complicados. No creo que el hecho de que las asignaciones, redondeadas a 0.01%, sumen 100.00%, diga algo sobre lo que sucederá cuando se redondeen al 0.1% o al 1% más cercano.

Otro enfoque sería hacer el cálculo inicial con redondeo a más cercano, y luego, si el resultado no da el 100%, divida todos los números por el porcentaje total y vuelva a intentarlo. Entonces, si el porcentaje final fue del 101%, divida todos los números (sin redondear) por 1.01 y repite la secuencia de redondeo y total. Esto dará resultados ligeramente diferentes que uno puede encontrar más o menos deseable. Supongamos que los números son 1.3 1.3 1.3 96.1. Cuando se redondea, esos 99 totales. Redondeando uno de los 1.3 hasta 2 haría el total de 100, pero el redondeo distorsionaría el valor en un 53% en lugar de un 23%; por el contrario, redondear 96.1 hasta 97 representaría una distorsión de alrededor de 0.95% de su valor (97 vs 96.1).

+0

Me gusta. La primera idea es una solución limpia y comprensible que será fácil de implementar correctamente. –

+0

Estoy volviendo a leer la segunda idea, y si bien parece un poco complicado, plantea un buen punto: ¿es mejor redondear 1.4 a 2.0 o 60.3 a 60.1? En otras palabras, ¿estamos tratando de minimizar la distorsión por asignación? –

+0

La segunda idea aquí no funciona para todos los conjuntos de datos. Usa estos números y pruébalo: [19.6,19.7,19.7,20.5,20.5] Su suma es 100, pero alrededor de ellos y se suman a 102. Escala hacia abajo y alrededor, se suman a 97. – Mnebuerquo

0

Creo que esto es lo que estás buscando. Probablemente podría ser limpiado y optimizado, pero toma la mayor distancia en el redondeo cuando se decide redondear un número en el otro sentido.

static List<double> Round2(List<double> actualAllocations) 
    { 
     List<double> actual = new List<double>(); 
     actual.AddRange(actualAllocations); 

     List<double> rounded = new List<double>(); 
     foreach (var a in actual) 
      rounded.Add(Math.Round(a)); 

     if (rounded.Sum() == 100) 
     { 
      return rounded; 
     } 
     else 
     { 
      bool roundUp = rounded.Sum() < 100; 

      for (int i = 0; i < Math.Abs(100 - rounded.Sum()); i++) 
      { 
       var index = actual.IndexOf(
        (from a in actual 
        orderby Math.Abs(Math.Round(a) - a) descending 
        select a).First()); 

       if (roundUp) 
        actual[index]++; 
       else 
        actual[index]--; 
      } 
     } 

     rounded.Clear(); 
     foreach (var a in actual) 
      rounded.Add(Math.Round(a)); 

     return rounded; 
    } 
Cuestiones relacionadas