PEP 450 – 표준 라이브러리에 통계 모듈 추가
- Author:
- Steven D’Aprano <steve at pearwood.info>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 01-Aug-2013
- Python-Version:
- 3.4
- Post-History:
- 13-Sep-2013
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 평균, 중앙값, 분산, 표준 편차 같은 일반적인 통계 함수를 위한 모듈을 Python 표준 라이브러리에 추가할 것을 제안합니다. http://bugs.python.org/issue18606 도 참고하십시오.
근거
제안하는 statistics 모듈은 Python 표준 라이브러리의 “배터리 포함(batteries included)” 철학에서 비롯되었습니다. Raymond Hettinger를 비롯한 여러 시니어 개발자들은 고급 통계 라이브러리와 임시방편 코드 사이 어딘가에 해당하는 양질의 통계 라이브러리를 요청해 왔습니다. [1] 평균, 표준 편차 등의 통계 함수는 누구나 떠올릴 만하고 유용한 배터리이며, 중등학교 학생이라면 누구나 익숙한 것입니다. 저렴한 공학용 계산기조차도 대개 다음과 같은 여러 통계 함수를 포함합니다:
- 평균
- 모분산과 표본 분산
- 모표준 편차와 표본 표준 편차
- 선형 회귀
- 상관 계수
중등학교 학생을 대상으로 하는 그래프 계산기는 대개 위의 모든 기능에 더해 다음 중 일부 또는 전부를 포함합니다:
- 중앙값
- 최빈값
- 정규, t, 카이제곱, F 배포에서 확률 변수의 확률을 계산하는 함수
- 평균에 대한 추론
그 밖의 기능도 있습니다 [2]. 마찬가지로 Microsoft Excel, LibreOffice, Gnumeric 같은 스프레드시트 애플리케이션도 풍부한 통계 함수 모음을 포함합니다 [3].
이와 대조적으로, 현재 Python에는 평균처럼 가장 단순하고 가장 명백한 통계 함수조차 계산하는 표준 방법이 없습니다. Python에서 통계 함수가 필요한 사람들에게는 두 가지 명백한 해결책이 있습니다:
- numpy 및/또는 scipy를 설치하는 것 [4];
- 또는 직접 만들어 쓰는 것입니다.
Numpy는 아마도 가장 기능이 풍부한 해결책이지만, 몇 가지 단점이 있습니다:
- 많은 용도에 비해 과할 수 있습니다. numpy 문서조차 다음과 같이 경고합니다“numpy에서 어떤 함수를 사용할 수 있는지 알기 어려울 수 있습니다. 이것은 완전한 목록은 아니지만, 대부분을 다루고 있습니다.”[5]
그리고 이어서 270개가 넘는 함수를 나열하는데, 그중 통계와 관련된 것은 극히 일부에 불과합니다.
- Numpy는 대규모 수치 작업을 하는 사람들을 대상으로 하므로, 계산 수학과 컴퓨터 과학에 대한 배경지식이 없는 사람에게는 부담스러울 수 있습니다. 예를 들어,
numpy.mean은 네 개의 인자를 받습니다:mean(a, axis=None, dtype=None, out=None)
다행히 초보자나 가끔 numpy를 쓰는 사용자에게는 이 중 세 개가 선택 사항이며, 간단한 경우에는
numpy.mean이 올바르게 동작합니다:>>> numpy.mean([1, 2, 3, 4]) 2.5
- 많은 사람에게 numpy 설치는 어렵거나 불가능할 수 있습니다. 예를 들어, 기업 환경에 있는 사람들은 서드파티 소프트웨어 설치를 허가받기 전에 어렵고 시간이 많이 드는 절차를 거쳐야 할 수 있습니다. 가끔 Python을 쓰는 사용자가 숫자 리스트의 평균을 구하려고 서드파티 패키지 설치 방법까지 배워야 한다는 것은 안타까운 일입니다.
이는 두 번째 선택지인 직접 만든(DIY) 통계 함수로 이어집니다. 흔히 쓰이는 통계 함수가 단순해 보이기 때문에, 언뜻 보기에는 매력적인 선택지처럼 보입니다. 예를 들면 다음과 같습니다:
def mean(data):
return sum(data)/len(data)
def variance(data):
# Use the Computational Formula for Variance.
n = len(data)
ss = sum(x**2 for x in data) - (sum(data)**2)/n
return ss/(n-1)
def standard_deviation(data):
return math.sqrt(variance(data))
위 코드는 간단히 테스트해 보면 올바른 것처럼 보입니다:
>>> data = [1, 2, 4, 5, 8]
>>> variance(data)
7.5
하지만 모든 데이터 점에 상수를 더해도 분산은 변하지 않아야 합니다:
>>> data = [x+1e12 for x in data]
>>> variance(data)
0.0
그리고 분산은 절대로 음수가 되어서는 안 됩니다:
>>> variance(data*100)
-1239429440.1282566
이와 대조적으로, 제안된 참조 구현은 처음 두 예제에 대해 정확히 올바른 답인 7.5를 구하고, 세 번째 예제에 대해서도 상당히 근접한 답인 6.012를 구합니다. numpy도 이보다 낫지 않습니다 [6].
단순한 통계 계산조차 부주의한 사람을 노리는 함정을 품고 있으며, 그 시작은 바로 계산 공식(Computational Formula) 자체입니다. 이름과는 달리 이 공식은 수치적으로 불안정하며, 위에서 볼 수 있듯이 극도로 부정확할 수 있습니다. 이 공식은 컴퓨터로 계산하기에는 전혀 적합하지 않습니다 [7]. 이 문제는 Python뿐만 아니라 [8] 많은 프로그래밍 언어의 사용자를 괴롭히는데, 코드를 작성하는 사람들이 수치적으로 부정확한 똑같은 코드를 계속해서 다시 만들어 내거나 [9], 다른 사람들에게 그렇게 하라고 조언하기 때문입니다 [10].
분산과 표준편차만의 문제가 아닙니다. 평균조차도 보기만큼 간단하지는 않습니다. 위 구현은 너무 단순해서 문제가 없을 것처럼 보이지만, 실제로는 문제가 있습니다:
- 내장
sum은 크기가 매우 다른 부동소수점 수를 다룰 때 정확도를 잃을 수 있습니다. 그 결과, 위의 단순한mean은 이 “고문 테스트”를 통과하지 못합니다:assert mean([1e30, 1, 3, -1e30]) == 1
1 대신 0을 반환하는데, 이는 순전히 계산상의 오류로 인한 100%의 오차입니다.
mean안에서math.fsum을 사용하면 부동소수점 데이터에 대해 더 정확해지지만, 불필요한 경우에도 모든 인자를 float로 변환하는 부작용이 생깁니다. 예를 들어, Fraction 리스트의 평균은 float가 아니라 Fraction일 것으로 기대해야 합니다.
위의 평균 구현이 단순한 분산 구현만큼 파국적으로 실패하지는 않지만, 표준 라이브러리 함수는 직접 만든 버전보다 훨씬 더 잘할 수 있습니다.
위 예제는 특히 나쁜 데이터 집합을 사용하지만, 보다 현실적인 데이터 집합에서도 정확도는 중요합니다. 데이터의 변동을 해석하는 첫 단계(조건이 나쁜 데이터를 다루는 것도 포함)는 흔히 데이터를 분산이 1(그리고 흔히 평균이 0)인 계열로 표준화하는 것입니다. 이 표준화에는 원시 계열의 평균과 분산을 정확하게 계산하는 것이 필요합니다. 평균과 분산을 단순하게 계산하면 정밀도를 매우 빠르게 잃을 수 있습니다. 정밀도가 정확도의 한계를 정하므로, 평균과 분산을 계산할 때 실용적인 범위에서 가장 정밀한 알고리즘을 사용하는 것이 중요하며, 그렇지 않으면 표준화 결과 자체가 쓸모없어집니다.
다른 언어/패키지와의 비교
제안된 통계 라이브러리는 numpy/scipy 같은 서드파티 라이브러리나, Minitab, SAS, Matlab처럼 전문 통계학자를 대상으로 하는 모든 기능을 갖춘 상용 통계 패키지와 경쟁하려는 것이 아닙니다. 이 라이브러리는 그래프 계산기나 공학용 계산기 수준을 목표로 합니다.
대부분의 프로그래밍 언어는 통계 함수를 내장 기능으로 거의 또는 전혀 지원하지 않습니다. 몇 가지 예외는 다음과 같습니다.
R
R(그리고 그 독점 사촌 격인 S)은 통계 작업을 위해 설계된 프로그래밍 언어입니다. R은 통계학자들 사이에서 매우 인기가 높으며 기능이 매우 풍부합니다 [11].
C#
C# LINQ 패키지에는 열거 가능 객체의 평균을 계산하는 확장 메서드가 포함되어 있습니다 [12].
Ruby
Ruby에는 어느 정도 분명한 수요가 있음에도 표준 통계 모듈이 포함되어 있지 않습니다 [13]. Statsample은 R과 경쟁하는 것을 목표로 하는, 기능이 풍부한 서드파티 라이브러리로 보입니다 [14].
PHP
PHP에는 기능이 매우 풍부한(대부분 문서화되어 있지는 않지만) 고급 통계 함수 모음이 있습니다 [15].
Delphi
Delphi는 Math 라이브러리에 Mean, Sum, Variance, TotalVariance, MomentSkewKurtosis를 비롯한 표준 통계 함수를 포함하고 있습니다 [16].
GNU 과학 라이브러리(GNU Scientific Library)
GNU 과학 라이브러리(GNU Scientific Library)에는 표준 통계 함수, 백분위수, 중앙값 등이 포함되어 있습니다 [17]. 제가 GSL에서 빌려 온 혁신 중 하나는 분산과 표준 편차를 계산할 때 호출자가 미리 계산한 표본 평균(또는 선험적으로 알려진 모평균)을 선택적으로 지정할 수 있게 하는 것입니다 [18].
모듈의 설계 결정
처음부터 모든 것을 포함하려 하기보다는 작게 시작하여 필요에 따라 라이브러리를 키워 나가는 것이 제 의도입니다. 따라서 현재 참조 구현에는 평균, 분산, 표준 편차, 중앙값, 최빈값 등 소수의 함수만 포함되어 있습니다. (전체 목록은 참조 구현을 참고하십시오.)
저는 다음과 같은 설계 특성을 목표로 했습니다.
- 속도보다 정확성을 우선합니다. 빠르지만 버그가 있는 함수를 고치는 것보다 정확하지만 느린 함수의 속도를 높이는 것이 더 쉽습니다.
- 한 번만 순회하는 알고리즘을 위해 정확성을 희생할 위험을 감수하기보다는, 데이터를 두 번 순회할 수 있도록 시퀀스 형태의 데이터에 집중합니다. 함수는 데이터가 리스트나 기타 시퀀스로 전달될 것으로 예상하며, 이터레이터가 주어지면 내부적으로 리스트로 변환할 수 있습니다.
- 함수는 가능한 한 모든 종류의 숫자 데이터를 존중해야 합니다. 예를 들어 Decimal 리스트의 평균은 float가 아니라 Decimal이어야 합니다. 이것이 불가능한 경우에는 float를 “최소 공통 데이터 타입”으로 취급합니다.
- 함수가 float, Decimal, Fraction 데이터 집합을 지원하기는 하지만, 혼합된 데이터 집합이 지원된다는 보장은 없습니다. (하지만 한편으로는 명시적으로 거부되지도 않습니다.)
- 기본 개념은 이해하지만 (예를 들어) 어떤 분산(모분산인지 표본 분산인지?)을 사용해야 하는지는 모를 수 있는 독자를 대상으로 충분한 문서를 제공합니다. 수학자와 통계학자는 표기법과 용어 모두에서 일관성이 없는 끔찍한 습관을 가지고 있으며 [19], 통용되는 모순되고 혼란스러운 정의를 이해하느라 많은 시간을 보낸 만큼, 제가 이 주제를 더 모호하게 만들기보다는 명확하게 하도록 최선을 다하는 것이 마땅합니다.
- 하지만 지루한 [20] 수학적 세부 사항까지 들어가는 것은 피합니다.
API
라이브러리의 초기 버전은 일변량(단일 변수) 통계 함수를 제공합니다. 일반적인 API는 function(data, ...) -> result 형태의 함수형 모델에 기반하며, 여기서 data는 (대개) 수치 데이터로 이루어진 필수 이터러블입니다.
저자는 리스트가 가장 흔히 사용되는 데이터 타입이 될 것으로 예상하지만, 어떤 이터러블 타입이든 허용되어야 합니다. 필요한 경우 함수는 내부적으로 리스트로 변환할 수 있습니다. 가능한 경우 함수는 데이터 값의 타입을 보존해야 합니다. 예를 들어, Decimal 리스트의 평균은 float가 아니라 Decimal이어야 합니다.
평균, 중앙값, 최빈값 계산하기
mean, median*, mode 함수는 단일 필수 인자를 받아 해당하는 통계량을 반환합니다. 예를 들면 다음과 같습니다.:
>>> mean([1, 2, 3])
2.0
제공되는 함수는 다음과 같습니다.
mean(data)- data의 산술 평균입니다.
median(data)- data의 중앙값(가운데 값)으로, 값의 개수가 짝수일 때는 가운데 두 값의 평균을 취합니다.
median_high(data)- data의 높은 중앙값으로, 항목 수가 짝수일 때 가운데 두 값 중 더 큰 값을 취합니다.
median_low(data)- data의 낮은 중앙값으로, 항목 수가 짝수일 때 가운데 두 값 중 더 작은 값을 취합니다.
median_grouped(data, interval=1)- 보간법을 사용하여 구한, 그룹화된 data의 50번째 백분위수입니다.
mode(data)- 가장 흔한 data 값입니다.
mode는 데이터 인자가 수치여야 한다는 규칙의 유일한 예외입니다. 이 함수는 문자열과 같은 명목형 데이터의 이터러블도 받습니다.
분산과 표준 편차 계산하기
공학용 계산기와 비슷하게, 통계 모듈은 모집단과 표본의 분산 및 표준 편차를 위한 별도의 함수를 포함합니다. 네 함수 모두 비슷한 시그니처를 가지며, 수치 데이터의 이터러블인 단일 필수 인자를 받습니다. 예를 들면 다음과 같습니다.:
>>> variance([1, 2, 2, 2, 3])
0.5
네 함수 모두 두 번째 선택적 인자로 데이터의 평균도 받습니다. 이는 GNU 과학 라이브러리(GNU Scientific Library)가 제공하는 비슷한 API를 본뜬 것입니다 [18]. 이 인자를 사용하는 경우는 다음 세 가지이며, 순서에는 특별한 의미가 없습니다:
- 평균값을 사전에(a priori) 알고 있습니다.
- 이미 평균을 계산했으며, 다시 계산하는 것을 피하고 싶습니다.
- 분산 함수를 (남)용하여 평균이 아닌 어떤 주어진 점에 대한 2차 모멘트를 계산하고 싶습니다.
각 경우에 주어진 인자가 의미 있는 값인지 확인하는 것은 호출자의 책임입니다.
제공되는 함수는 다음과 같습니다:
variance(data, xbar=None)- data의 표본 분산이며, 선택적으로 xbar를 표본 평균으로 사용합니다.
stdev(data, xbar=None)- data의 표본 표준 편차이며, 선택적으로 xbar를 표본 평균으로 사용합니다.
pvariance(data, mu=None)- data의 모집단 분산이며, 선택적으로 mu를 모집단 평균으로 사용합니다.
pstdev(data, mu=None)- data의 모집단 표준 편차이며, 선택적으로 mu를 모집단 평균으로 사용합니다.
기타 함수
공개 함수가 하나 더 있습니다.
sum(data, start=0)- 수치 data의 고정밀 합계입니다.
명세
제안된 참조 구현은 순수 Python으로 작성되어 있으므로, 다른 Python 구현체는 이 모듈을 수정 없이 쉽게 사용하거나 필요에 맞게 조정할 수 있습니다.
모듈의 이름은 무엇이어야 합니까?
이 모듈은 최상위 모듈 statistics가 됩니다.
math를 패키지로 바꾸고 이 모듈을 math의 하위 모듈로 만들자는 의견도 일부 있었지만, 결국 최상위 모듈로 하자는 데 전반적인 합의가 이루어졌습니다. 후보로 거론되었으나 채택되지 않은 다른 이름으로는 stats (기존 stat 모듈과 혼동될 위험이 너무 큼)와 statslib (“너무 C 같다”는 평가를 받음)가 있었습니다.
논의 및 해결된 문제
이 제안은 이전에 여기에서 논의되었습니다 [21].
Python-Ideas에서의 논의와 초기 코드 리뷰 과정에서 여러 설계 문제가 해결되었습니다. 표준 라이브러리에 또 다른 sum 함수를 추가하는 것에 대해 많은 우려가 있었습니다. 자세한 내용은 아래의 FAQ를 참조하십시오. 또한 sum의 초기 구현은 Decimal을 다룰 때 몇 가지 반올림 문제와 기타 설계 문제를 겪었습니다. 이 문제를 해결하는 데 Oscar Benjamin의 도움이 매우 귀중했습니다.
또 다른 문제는 이터레이터 형태의 데이터를 처리하는 방식이었습니다. 분산의 첫 번째 구현은 데이터가 이터레이터 형태인지 시퀀스 형태인지에 따라 단일 패스 알고리즘과 2-패스 알고리즘 사이를 조용히 전환했습니다. 사용된 알고리즘에 따라 계산된 분산이 약간 달라질 수 있었기 때문에 이는 설계상의 실수로 판명되었으며, variance 등은 내부적으로 리스트를 생성하여 항상 더 정확한 2-패스 구현을 사용하도록 변경되었습니다.
논란이 된 설계 중 하나는 중앙값을 계산하는 함수들과 관련된 것으로, 이 함수들은 median, median.low, median.high 등과 같이 median 호출 가능 객체의 속성으로 구현되었습니다. 표준 라이브러리의 unittest.mock에 이 스타일을 사용하는 기존 사례가 적어도 하나 있기는 하지만, 코드 리뷰어들은 이 방식이 표준 라이브러리에는 너무 이례적이라고 판단했습니다. 그 결과, 설계는 median_low, median_high 등과 같이 유사 네임스페이스 명명 규칙을 따르는 별도의 함수들로 이루어진 보다 전통적인 설계로 변경되었습니다.
코드 리뷰어들이 우려한 또 다른 문제는 연속 데이터의 표본 최빈값을 계산하는 함수가 있다는 점이었습니다. 일부는 알고리즘 선택에 의문을 제기했고, 이 함수를 포함할 만큼 흔히 필요한 기능인지도 문제 삼았습니다. 그래서 이 함수는 API에서 제외되었으며, 이제 mode는 고유한 값의 개수를 세는 기본적인 교과서식 알고리즘만 구현합니다.
또 다른 중요한 논의 사항은 timedelta 객체의 통계량을 계산하는 것이었습니다. statistics 모듈이 timedelta 객체를 직접 지원하지는 않지만, 먼저 timedelta.total_seconds 메서드로 숫자로 변환하면 이 사용 사례를 지원할 수 있습니다.
자주 묻는 질문
이 모듈은 표준 라이브러리 포함을 검토하기 전에 PyPI에서 일정 기간을 거쳐야 하지 않습니까?
이 모듈의 이전 버전은 2010년부터 PyPI [22]에서 제공되어 왔습니다. numpy보다 훨씬 단순하므로 여러 해에 걸친 외부 개발이 필요하지 않습니다.
표준 라이브러리에 또 다른 버전의 sum이 정말 필요합니까?
이 부분은 참조 구현에서 가장 논란이 많은 부분으로 드러났습니다. 어떤 의미에서는 sum이 세 개나 있는 것은 분명히 두 개가 너무 많습니다. 하지만 다른 의미에서는 필요합니다. 기존의 두 버전이 적합하지 않은 이유는 여기 [23]에 설명되어 있지만, 간단히 요약하면 다음과 같습니다:
- 내장 sum은 float에서 정밀도를 잃을 수 있습니다;
- 내장 sum은 문자열과 바이트열을 제외하고
+연산자를 지원하는 모든 비숫자 데이터 타입을 받아들입니다; math.fsum은 정밀도가 높지만 모든 인자를 float으로 강제 변환합니다.
기존 sum 중 어느 하나를 “수정”하는 데 관심을 보인 사람들이 있었습니다. 이것이 3.4 기능 동결 전에 이루어진다면 statistics.sum을 유지하기로 한 결정을 재검토할 수 있습니다.
이 모듈이 이전 버전의 Python으로 백포트됩니까?
이 모듈은 현재 3.3을 대상으로 하며, 당분간 3.3용으로 PyPI에서 제공할 예정입니다. 3.x 시리즈의 이전 버전으로 백포트할 가능성은 높습니다(아직 결정되지는 않았습니다). 2.7로 백포트할 가능성은 그보다 낮지만 배제하지는 않았습니다.
이 모듈은 numpy를 대체하려는 것입니까?
아닙니다. 수년에 걸쳐 커질 가능성은 있지만(아래의 미해결 문제를 참고하십시오), numpy를 대체하거나 numpy와 직접 경쟁하는 것을 목표로 하지는 않습니다. Numpy는 전문가를 대상으로 하는 완전한 기능을 갖춘 수치 라이브러리로, Python 생태계에서 수치 라이브러리의 원자로와 같은 존재입니다. 이 모듈은 “배터리 포함(batteries included)”이라는 말에서의 배터리 하나일 뿐이며, “numpy를 사용하라”와 “직접 만들어 쓰라” 사이 어딘가의 중간 수준을 목표로 합니다.
향후 작업
- 현 단계에서는 선형 회귀, 상관 계수, 공분산과 같은 다변량 통계 함수에 가장 적합한 API가 무엇인지 확신하지 못합니다. 가능한 API는 다음과 같습니다:
- x 데이터와 y 데이터에 대한 별도의 인자:
function([x0, x1, ...], [y0, y1, ...])
- (x, y) 데이터에 대한 단일 인자:
function([(x0, y0), (x1, y1), ...])
이 API는 GvR [24]이 선호하는 방식입니다.
- 2차원 배열에서 임의의 열 선택하기:
function([[a0, x0, y0, z0], [a1, x1, y1, z1], ...], x=1, y=2)
- 위 방법들을 조합한 방식입니다.
다변량 통계에 적합한 API에 대한 합의가 없으므로, 이러한 다변량 함수는 Python 3.5까지 포함을 미루겠습니다.
- x 데이터와 y 데이터에 대한 별도의 인자:
- 마찬가지로, 확률 변수의 확률을 계산하는 함수와 추론 검정(예: 스튜던트 t-검정) 함수도 3.5까지 미룹니다.
- 이터레이터 형태의 데이터를 리스트로 변환하지 않고도 한 번의 순회로 여러 통계량을 계산할 수 있는 단일 패스 함수를 포함하는 데 상당한 관심이 있습니다. PyPI에 있는 실험적인
stats패키지에는 통계 함수의 코루틴 버전이 포함되어 있습니다. 이러한 함수의 포함은 3.5로 미룹니다.
참고 문헌
Copyright
This document has been placed in the public domain.