“Don’t use hands to do things that can be done efficiently by the computer.” [Tom Duff, Bell Labs]
Abstract
How can I make sure that the sum of my rounded percentages sums up to 100% exactly? Can I ensure for accounting purposes that my distribution of overhead costs exactly matches the original cost sum? These questions are well known and have been analysed for a long time. This article presents an easy-to-use solution with Excel / VBA. The function presented can round relative values (percentages) to 100% exactly, and it can round absolute values (cost distributions, for example) while preserving their original rounded sum. A parameter drives which type of error versus common half-up rounding you like to minimize: the absolute error, the relative error, or the weighted absolute error.
Percentage Example
The values 11, 45, and 555 with the sum 611 show a percentage sum not of 100.00 but of 99.99 if percentages are rounded to 2 digits:
Values | Percent | |
---|---|---|
11 | 1.80 | |
45 | 7.36 | |
555 | 90.83 | |
Sum | 611 | 99.99 |
The Excel / VBA function call RoundToSum({11,45,555},2,FALSE) would result in {1.80,7.37,90.83}, though. Here the percentage value 7.364975 was rounded to the “wrong” side to achieve the percentage sum of 100.00 and to minimize the absolute error in comparison to half-up rounding. With RoundToSum({11,45,555},2,FALSE,3) we would have received {1.80,7.36,90.84} because the parameter 3 would have minimized the weighted absolute error.
Example with Absolute Values
The sum of column 2 differs by +2.000 from the rounded sum. Bold values in non-sum cells have been amended by the function RoundToSum:
Minimize Absolute Error | Minimize Relative Error | Minimize Weighted Absolute Error | |||
---|---|---|---|---|---|
Values | Round to 1,000 | RoundToSum (…,-3,…,1) | RoundToSum (…,-3,…,2) | RoundToSum (…,-3,…,3) | |
4,523 | 5,000 | 5,000 | 5,000 | 5,000 | |
456 | 0 | 0 | 0 | 0 | |
-78,845 | -79,000 | -79,000 | -79,000 | -79,000 | |
-14,491 | -14,000 | -15,000 | -14,000 | -14,000 | |
65,789 | 66,000 | 66,000 | 66,000 | 66,000 | |
129,512 | 130,000 | 129,000 | 130,000 | 129,000 | |
15,562 | 16,000 | 16,000 | 16,000 | 16,000 | |
548,555 | 549,000 | 549,000 | 549,000 | 548,000 | |
1,590 | 2,000 | 2,000 | 1,000 | 2,000 | |
-897 | -1,000 | -1,000 | -2,000 | -1,000 | |
6,968 | 7,000 | 7,000 | 7,000 | 7,000 | |
2,987 | 3,000 | 3,000 | 3,000 | 3,000 | |
Sum | 681,709 | 684,000 | 682,000 | 682,000 | 682,000 |
The Excel / VBA Function RoundToSum
Name
RoundToSum – Rounding values preserving their rounded sum
Synopsis
RoundToSum(vInput, [lDigits], [bAbsSum], [lErrorType], [bDontAmend])
Description
RoundToSum rounds summands without changing their rounded sum. It applies the largest remainder method to minimize the error versus the commonly used half-up rounding method. In case this error is identical for one or more summands the first summand(s) encountered will be amended.
Please note: The solution presented here is limited to one-dimensional tables. For tables of higher dimensions or for tables with subtotals there exists no general solution.
Parameters
vInput – Range or array containing the unrounded input values (summands).
lDigits – Optional, default value is 2. Number of digits to round to. For example: 0 rounds to integers, 2 rounds to the cent, -3 will use thousands.
bAbsSum – Optional, default value is TRUE. TRUE takes the summands as they are. FALSE works on the summands' percentages to make all percentages add up to 100% exactly.
lErrorType – Optional, default value is 1. Type of error to be minimized: 1 – absolute error; 2 – relative error; 3 – weighted difference (weighted absolute error).
bDontAmend – Optional, default value is FALSE. TRUE does not amend the rounded summands to match the rounded sum. FALSE performs amendments as described above. This parameter is mainly for ease of use in presentations to see this function’s impact.
Literature
Diaconis, P., & Freedman, D. (2007, July 13). On Rounding Percentages. Retrieved from (external link!) http://statweb.stanford.edu/~cgates/PERSI/papers/freedman79.pdf
Sande, G. (2005, August 7). Guaranteed Controlled Rounding for Many Totals in Multi-way and Hierarchical Tables. Retrieved from (external link!) https://nces.ed.gov/FCSM/pdf/2005FCSM_Sande_IXA.pdf
Plumhoff, B. (Controller Magazin 01/2022, Haufe Verlag). Werte runden ohne deren gerundete Summe zu ändern. The original article in German.
Appendix – RoundToSum Code
Please read my Disclaimer.
Function RoundToSum(vInput As Variant, Optional lDigits As Long = 2, Optional bAbsSum As Boolean = True, _
Optional lErrorType As Long = 1, Optional bDontAmend As Boolean = False) As Variant
'Calculate rounded summands which exactly add up to the rounded sum of unrounded summands.
'It uses the largest remainder method which minimizes the error to the original unrounded summands.
'This function needs to be entered as an array formula into the cells for the rounded summands.
'Source (EN): http://www.sulprobil.com/roundtosum_en/
'Source (DE): http://www.bplumhoff.de/roundtosum_de/
'(C) (P) by Bernd Plumhoff V1.3 PB 09-Jun-2021
Dim i As Long, j As Long, k As Long, n As Long, lCount As Long, lSgn As Long
Dim d As Double, dDiff As Double, dRoundedSum As Double, dSumAbs As Double: Dim vA As Variant
With Application.WorksheetFunction
vA = .Transpose(.Transpose(vInput)): On Error GoTo Errhdl: i = vA(1) 'Force error in case of vertical arrays
On Error GoTo 0: n = UBound(vA): ReDim vC(1 To n) As Variant, vD(1 To n) As Variant: dSumAbs = .Sum(vA)
For i = 1 To n
d = IIf(bAbsSum, vA(i), vA(i) / dSumAbs * 100#): vC(i) = .Round(d, lDigits)
If lErrorType = 1 Then 'Absolute error
vD(i) = vC(i) - d
ElseIf lErrorType = 2 Then 'Relative error
vD(i) = IIf(d = 0#, 0#, (vC(i) - d) / d)
ElseIf lErrorType = 3 Then 'Weighted difference = Weighted absolute error
vD(i) = (vC(i) - d) * d
Else
RoundToSum = CVErr(xlErrValue): Exit Function
End If
Next i
If Not bDontAmend Then
dRoundedSum = .Round(IIf(bAbsSum, dSumAbs, 100#), lDigits)
dDiff = .Round(dRoundedSum - .Sum(vC), lDigits)
If dDiff <> 0# Then
lSgn = Sgn(dDiff): lCount = .Round(Abs(dDiff) * 10 ^ lDigits, 0)
'Now find highest (lowest) lCount indices in vC
ReDim m(1 To lCount) As Long
For i = 1 To lCount: m(i) = i: Next i
For i = 1 To lCount - 1
For j = i + 1 To lCount
If lSgn * vD(i) > lSgn * vD(j) Then k = m(i): m(i) = m(j): m(j) = k
Next j
Next i
For i = lCount + 1 To n
If lSgn * vD(i) < lSgn * vD(m(lCount)) Then
j = lCount - 1
Do While j > 0
If lSgn * vD(i) >= lSgn * vD(m(j)) Then Exit Do
j = j - 1
Loop
For k = lCount To j + 2 Step -1: m(k) = m(k - 1): Next k: m(j + 1) = i
End If
Next i
For i = 1 To lCount: vC(m(i)) = .Round(vC(m(i)) + dDiff / lCount, lDigits): Next
End If
End If
RoundToSum = vC
If TypeName(Application.Caller) = "Range" Then
If Application.Caller.Rows.Count > Application.Caller.Columns.Count Then
RoundToSum = .Transpose(vC) 'It's two-dimensional with 2nd dim const = 1
End If
End If
Exit Function
Errhdl:
'Transpose variants to be able to address them with vA(i), not vA(i,1)
vA = .Transpose(vA): Resume Next
End With
End Function
Please read my Disclaimer.
RoundToSum.xlsm [51 KB Excel file, open and use at your own risk]