흥미로운 주제로 혼자 알기가 아까운 마음이 들었습니다. 더불어 round-half-even을 사용하는 이유에 대해 다룬 글을 찾기가 어려워, 이러한 점에 관심 있는 분들께 도움이 되고자 글을 남깁니다.
다음 코드의 결과는 무엇이라고 생각하십니까?
print(round(9/2))
print(round(7/2))
print(round(3/2))
아마 대부분의 사람들은 5, 4, 2를 말하지 않을까 싶습니다. 놀랍게도 이 구문의 실행결과는 4,4,2입니다.
왜냐하면 파이썬의 round는 round-half-even의 방식을 따르기 때문이죠. 정확히는 round-half-even을 따른다기보다는, round-half-even이 되는 상황에서는 round-half-even을 이용한다는 것이 맞는 말인 것 같습니다.
round-half-even 방식이란?
round-half-even 방식이란, 본 값에서 올림과 내림이 정확히 같은 차이를 보일 때 짝수에 가까운 쪽으로 올림을 하는 것입니다.
앞서 말씀드린 예시를 보고 다시 설명하자면, 다음과 같습니다.
9/2 = 4.5
- 올림(round-up) :5
- 내림(round-down):4
4.5에서 올림까지 +0.5, 내림까지 -0.5
짝수에 가까운 4로 반올림
7/2 = 3.5
- 올림(round-up) :4
- 내림(round-down):3
3.5에서 올림까지 +0.5, 내림까지 -0.5
짝수에 가까운 4로 반올림
3/2 = 1.5
- 올림(round-up) :2
- 내림(round-down):1
1.5에서 올림까지 +0.5, 내림까지 -0.5
짝수에 가까운 2로 반올림
round-half-even를 사용하는 이유
우리가 일상적으로 사용하는 반올림은 반올림의 한 방법일 뿐입니다. 반올림은 생각보다 여러 가지 방법을 가지고 있습니다.
- 정수로 직접 반올림( 내림, 반올림, 0으로 반올림, 0에서 반올림)
- 가장 가까운 정수로 반올림(반올림, 반내림, 0쪽으로 반올림 등)
- 정수로 무작위 반올림(무작위, 교대로, 확률적)
다만 위의 그림을 참고할 때 올림은 항상 +∞(양의 무한대)로, 내림은 항상 -∞(음의 무한대로) 근사되기 때문에 이에 대한 오차가 생길 수 밖에 없습니다.
예컨대, 1/3과 같은 값들은 올림을 택하든, 내림을 택하든 항상 오차가 발생해 정확한 값을 나타낼 수 없습니다.
그래서 수학적으로 이에 대한 오차의 편향을 최대한 줄이고자 고안된 것이 round-half-even
입니다.
round-half-even에 대한 대책
왜 round-half-even
이 사용됐는지를 이해하면 대책이라 말하기가 뭐하지만, 어쨌든 원하는 값으로 올림을 하고 싶다면 이 문제를 잘 다스릴 필요가 있습니다.
이에 대한 방법은 크게 다음의 두 가지 방법입니다.
- 함수를 직접 정의해 0.5를 더 해 int로 출력해주는 함수를 만들거나
def HalfRoundUp(value):
return int(value + 0.5)
- decimal를 이용하는 방법
from decimal import Decimal, ROUND_HALF_UP
Decimal(1.5).quantize(0, ROUND_HALF_UP)
# This also works for rounding to the integer part:
Decimal(1.5).to_integral_value(rounding=ROUND_HALF_UP)
개인적으로 전자의 방법은 직접적으로 값을 변형하기 때문에 추천드리는 방법은 아닙니다. 되도록이면 후자의 방법을 사용하는 것을 추천드립니다.
공식문서를 참조하면 decimal의 여러 반올림 모듈을 이용할 수 있습니다.
기본으로 설정되어 있는 모듈은 ROUND_HALF_EVEN입니다.
print(decimal.getcontext())
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
다음과 같이 모듈을 바꿔서 이용할 수 있습니다.
print(decimal.getcontext())
print(Decimal('4.5').quantize(Decimal('1.')))
decimal.getcontext().rounding = ROUND_HALF_UP
print(decimal.getcontext())
print(Decimal('4.5').quantize(Decimal('1.')))>> Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>> 4>> Context(prec=28, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])
>> 5