views:

145

answers:

5

As the title describes, I have a set of objects - call them Allocations - which contain a description & a number. All numbers in the set add up to 100%, but for display purpose I sometimes round to a whole percent. In some edge cases, having rounded the numbers I end up with 99%.

Example:

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!)

The requested solution, which is imperfect but will do, is to find the highest one to be rounded down, and round it up instead. In the example above, 1.42% would become 2% when rounded. Edit: By "Highest one to be rounded down" I mean the one which is being rounded the furthest. Hence 1.42% is being rounded down by 0.42 whereas the 65.23 is only being rounded down 0.23

So now the code, I have a class

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

And these are held in an IEnumerable<Allocation>. So, potentially using LINQ, how can I determine which one is the one to round up. Or more specifically, how can I generate a new IEnumerable<Allocation> with the numbers rounded.

If anyone has any other suggestion for always making rounded percentage always equate to 100% that would be even better!

+2  A: 

Regarding getting 100%, why not run the original calculation once first and see what percentage you get and then you know how many you need to round up vs down by seeing how many percentage points it differs from 100%.

So if you end up with 97%, round 3 numbers up instead of down. Or if you end up with 102%, round the two numbers with the lowest decimals (over 0.5) down instead of up.

ho1
Just to clarify, It can only ever be 99% or 100% - and determining that I have a 99% situation is not a problem - its knowing which item in the set to round up instead of down.
Jamiec
@Jamiec: Fair enough, but just out of curiosity, are you really sure it can only be 99% or 100%? Do you have some other check to make sure that the values could not have been something like: 65.47, 25.51, 7.51 and 1.51?
ho1
Agree with ho1. Are you saying it can only be 99 because that's all you've encountered or is there actually a business rule preventing it? Look at 50.50 + 49.50 for a 101 case. Look at 100% / 16 for a case where rounded = 100 and unrounded = 96.
Mike M.
Ok, to add a bit of domain knowledge to this thought exercise. These percentages represent Allocation of asset classes in a portfolio. The percentages expressed with 2dp accuracy always end up as 100%, fact. My problem arises as for one particlar piece of functionality I must first round the asset allocations to 0dp accuracy but it must still add up to 100%. Logically I think I can only end up with 99% or 100% but I could be wrong.
Jamiec
@Mike M - seems you've prooved me wrong.. I can end up in the range 99-101 as demonstrated by your 50.50 + 49.50 case. Thanks for making my problem greater :) I now need a solution to artificially round up _or_ down. Thanks a bunch! j/k.
Jamiec
+1  A: 
 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)
                                  };
James Curran
Thanks for answer, it looked promising and similar to what im trying locally, except you get the higest number, rather than the one being rounded the furthest. See my edit to the OP.
Jamiec
@Jamiec: See my edit to my answer (PS: Growing up I was "Jamie C")
James Curran
@James Curren - My last name is Cohen. My real name is James. I cant tell you how many times my name is heard as "James Curran" on the telephone. Spoooky!
Jamiec
+3  A: 

As ho1 has indicated, the solution to add 1 to a specific row doesn't solve the real problem.

Consider these scenarios:

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

Here's some untested code that might get you close to where you need to go. There's probably a sign error in here so watch out.

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();

Note: Ordering by introduced error can result in ties. Consider the 3 items case, only one item will get +1... you may expect that item to be consistently chosen. If consistent results are expected from multiple runs, ties should be broken.

David B
+1, however, the OP noted that the actuals, @ 2dp are guaranteed to 100%, so your scenarios are impossible.
SnOrfus
Worst one is 67 equal measures:`67 items evenly divided, 100/67 = 1 ; 1 * 67 = 67 ; Error = -33`
Ax
@David B ~ my bad, when I said +1, I meant it... just delayed :) sorry. Also 100/7 = ~14.2857 and 14.29 * 7 = 100.03
SnOrfus
@David B~ That's not how I understand it. From what I can tell, he's receiving a list of numbers that are accurate to 2 decimal places (which all add up to 100% precisely) and he's tasked with rounding them to 0 decimal places, such that the sum of the rounded numbers is still 100%.
SnOrfus
@SnOrfus you are technically correct. 7*14.29 does not exactly = 100, and so does not fit within a narrow interpretation of the OP's situation. It remains a useful thought exercise for the problem of rounding a collection. If this is disturbing, consider instead six 14.29 plus one 14.26. Also - please overlook the fact that 14.29 cannot be exactly represented in a double.
David B
David, thanks for this - it helped me alot. Also pretty close to what I came up with having mulled it over for a night.
Jamiec
+3  A: 

I'd suggest always rounding down, and then if the result is 100-n, round up the numbers with the 'n' largest residuals. That will work for any data. Approaches which round to nearest and then try to adjust the result are apt to be more complicated. I don't think the fact that the allocations, rounded to 0.01%, add up to 100.00% says anything about what's going to happen when they are rounded to the nearest 0.1% or 1%.

Another approach would be to do the initial computation with round-to-nearest, and then if the result doesn't yield 100% divide all numbers by the total percentage and try again. So if the final percentage was 101%, divide all (unrounded) numbers by 1.01 and repeat the round-and-total sequence. This will give slightly different results which one may find more or less desirable. Suppose the numbers are 1.3 1.3 1.3 96.1. When rounded, those total 99. Rounding one of the 1.3's up to 2 would make the total 100, but rounding would distort the value by 53% rather than 23%; by contrast, rounding 96.1 up to 97 would represent about a 0.95% distortion of its value (97 vs 96.1).

supercat
I like it. The first idea is a clean, understandable solution that'll be easy to implement correctly.
Steven Sudit
I'm re-reading the second idea, and while it seems a bit complicated, it does raise a good point: is it better to round 1.4 to 2.0 or 60.3 to 60.1? In other words, are we trying to minimize distortion on a per-allocation basis?
Steven Sudit
The second idea here does not work for all data sets. Use these numbers and try it: [19.6,19.7,19.7,20.5,20.5] Their sum is 100, but round them and they add to 102. Scale them down and round, they add to 97.
Mnebuerquo
@Mnebuerquo: Fair point. The suggested scaling may overshoot, and will not necessarily fall into an attractor. The problem could be resolved via multiple passes which don't try to hit the mark perfectly, but home in on it. If there are multiple 'ties' in the original data, that might not work (e.g. if eight data items are precisely 12.5, there's no way to scale them so they'll total 100); in that case, falling back to the first approach would be necessary.
supercat
A: 

I think this is what you're looking for. It could probably be cleaned up and optimized, but it takes the largest distance in rounding when deciding to round a number the other way.

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