이진수 대신 십진수를 쓰면 오차가 없어질까?

algorithms
featured
float
decimal
double
BigDecimal
python
java
Author

Yunho Kee

Published

May 26, 2024

Modified

August 26, 2024

Intro

정수 아닌 실수 연산은 오차 관리가 더 힘들다. 십진수를 쓰면 문제가 없을까?

정수 오차

저장 용량에 따른 자릿수를 초과하면 이진법이나 십진법이나 오차가 있다.

실수 오차

정수 아닌 실수도 마찬가지인데 소수점 이하 자릿수가 무한일 수 있다: 무한소수.

five = 10 / 6 * 3
four = (7 - five) * 2

print(five, int(five))
print(four, int(four))
5.0 5
4.0 4
from decimal import Decimal, ROUND_DOWN

five = Decimal('10') / Decimal('6') * Decimal('3')
four = (Decimal('7') - five) * Decimal('2')

print(five, int(five), five.quantize(Decimal('0'), ROUND_DOWN))
print(four, int(four), four.quantize(Decimal('0'), ROUND_DOWN))
5.000000000000000000000000001 5 5
3.999999999999999999999999998 3 3

오차 관리

소수점 이하 무한한 자릿수가 모두 필요할 일은 거의 없다. 가령 소수점 이하 n번째 자리까지 필요하다면 그보다 작은 수를 가령 n + 1번째 자리에 1을 더하거나 빼 주면 된다. 더할 때는 절사, 절하, 내림, ROUND_DOWN이나 ROUND_HALF_UP이 필요할 때이다. 뺄 때는 절상, 올림, ROUND_UP이나 ROUND_HALF_DOWN이 요구될 때이다.

_EPSILON = 1e-2

five = 10 / 6 * 3
four = (7 - five) * 2 + _EPSILON

five_epsilon = five + _EPSILON
print(five_epsilon, int(five_epsilon))
print(four, int(four))
5.01 5
4.01 4
from decimal import Decimal, ROUND_DOWN

_EPSILON = Decimal('1e-2')

five = Decimal('10') / Decimal('6') * Decimal('3')
four = (Decimal('7') - five) * Decimal('2') + _EPSILON

five_epsilon = five + _EPSILON
print(five_epsilon, int(five_epsilon), five_epsilon.quantize(Decimal('0'), ROUND_DOWN))
print(four, int(four), four.quantize(Decimal('0'), ROUND_DOWN))
5.010000000000000000000000001 5 5
4.009999999999999999999999998 4 4

언어 초월

Java의 BigDecimal은 오차를 지역적으로 관리한다. 오차 관리가 필요할 때 Runtime에 ArithmeticException을 발생시킨다. 이처럼 반강제적인 예외 처리는 Python에서 발생할 오차를 일부 예방한다. 가령 연산 과정마다 주의하며 RoundingMode 그리고 scale이나 precision 등을 일일이 명시해야 지역적 오차로 인한 비정상 종료가 방지된다.

하지만 일부 오차는 지역적 결과에 Greedy하게 의존하는 관리가 불가능하다. 가령 다음 예시의 printNaiveBigDecimals에서 5와 4를 동시에 얻을 수 없고 4와 4 또는 5와 3밖에 얻지 못한다는 한계가 있다.

그래서 상기한 Epsilon 사용을 여전히 권장한다. 그러면 지역적 오차 관리 결과를 무시하고 원하는 다음 결과를 만들 수 있다. 즉 다음 예시의 printEpsilonBigDecimals에서 divideRoundingModeRoundingMode.DOWN, RoundingMode.UP 등 어느 것이었는지와 무관하게 5와 4를 동시에 만들 수 있다.

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

class DecimalRoundDown {

    public static void main(final String[] args) {

        printNaivePrimitiveDoubles();
        
        System.out.println();
        printNaiveBigDecimals(RoundingMode.DOWN);
        printNaiveBigDecimals(RoundingMode.UP);
        
        System.out.println();
        printEpsilonPrimitiveDoubles();
        
        System.out.println();
        printEpsilonBigDecimals(RoundingMode.DOWN);
        printEpsilonBigDecimals(RoundingMode.UP);

    }

    private static void printNaivePrimitiveDoubles() {

        final double five = 10. / 6 * 3;
        final double four = (7 - five) * 2;

        System.out.println(five + " " + (int) five);
        System.out.println(four + " " + (int) four);

    }

    private static void printNaiveBigDecimals(final RoundingMode divideRoundingMode) {

        BigDecimal five;
        try {
            five = new BigDecimal("10")
                    .divide(new BigDecimal("6"))
                    .multiply(new BigDecimal("3"));
        } catch (Exception e) {
            e.printStackTrace();
            five = new BigDecimal("10")
                    .divide(new BigDecimal("6"), 3, divideRoundingMode)
                    .multiply(new BigDecimal("3"));
        }
        final BigDecimal four = new BigDecimal("7")
                .subtract(five)
                .multiply(new BigDecimal("2"));

        final MathContext mc = new MathContext(2, RoundingMode.DOWN);
        System.out.println(five + " " + five.intValue() + " " + five.round(mc));
        System.out.println(four + " " + four.intValue() + " " + four.round(mc));

    }

    private static void printEpsilonPrimitiveDoubles() {

        final double EPSILON = 1e-2;

        final double five = 10. / 6 * 3;
        final double four = (7 - five) * 2 + EPSILON;

        final double fiveEpsilon = five + EPSILON;
        System.out.println(fiveEpsilon + " " + (int) fiveEpsilon);
        System.out.println(four + " " + (int) four);

    }

    private static void printEpsilonBigDecimals(final RoundingMode divideRoundingMode) {

        final BigDecimal EPSILON = new BigDecimal("1e-2");

        final BigDecimal five = new BigDecimal("10")
                .divide(new BigDecimal("6"), 3, divideRoundingMode)
                .multiply(new BigDecimal("3"));
        final BigDecimal four = new BigDecimal("7")
                .subtract(five)
                .multiply(new BigDecimal("2"))
                .add(EPSILON);

        final BigDecimal fiveEpsilon = five.add(EPSILON);
        final MathContext mc = new MathContext(2, RoundingMode.DOWN);
        System.out.println(fiveEpsilon + " " + fiveEpsilon.intValue() + " " + fiveEpsilon.round(mc));
        System.out.println(four + " " + four.intValue() + " " + four.round(mc));

    }

}
5.0 5
4.0 4

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
        at java.base/java.math.BigDecimal.divide(BigDecimal.java:1780)
        at DecimalRoundDown.printNaiveBigDecimals(DecimalRoundDown.java:39)
        at DecimalRoundDown.main(DecimalRoundDown.java:12)
4.998 4 4.9
4.004 4 4.0
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
        at java.base/java.math.BigDecimal.divide(BigDecimal.java:1780)
        at DecimalRoundDown.printNaiveBigDecimals(DecimalRoundDown.java:39)
        at DecimalRoundDown.main(DecimalRoundDown.java:13)
5.001 5 5.0
3.998 3 3.9

5.01 5
4.01 4

5.008 5 5.0
4.014 4 4.0
5.011 5 5.0
4.008 4 4.0
Back to top

Citation

BibTeX citation:
@online{kee2024,
  author = {Kee, Yunho},
  title = {이진수 대신 십진수를 쓰면 오차가 없어질까?},
  date = {2024-05-26},
  url = {https://yhkee0404.github.io/posts/algorithms/decimal-round-down/},
  langid = {ko}
}
For attribution, please cite this work as:
Kee, Yunho. 2024. “이진수 대신 십진수를 쓰면 오차가 없어질까?” May 26, 2024. https://yhkee0404.github.io/posts/algorithms/decimal-round-down/.