statistics — 수치 데이터의 수리통계 함수

statistics — 수치 데이터의 수리통계 함수

버전 3.4에 추가됨.

이 모듈은 숫자(Real-값) 데이터의 수리통계를 계산하는 함수들을 제공해요.

이 모듈은 NumPy, SciPy 같은 서드파티 라이브러리나 Minitab, SAS, Matlab처럼 전문 통계학자를 겨냥한 유료 완전기능 통계 패키지의 경쟁자가 되려는 게 아니에요. 그래프 계산기나 공학용 계산기 수준을 목표로 해요.

특별히 언급하지 않는 한, 이 함수들은 int, float, Decimal, Fraction을 지원해요. 다른 타입(수치 탑 안에 있든 없든)에 대한 동작은 현재 지원되지 않아요. 타입이 섞인 컬렉션에서의 동작도 정의되어 있지 않고 구현에 따라 달라져요. 입력 데이터가 혼합 타입이라면 map()을 써서 일관된 결과를 보장할 수 있어요. 예: map(float, input_data).

일부 데이터셋은 누락된 데이터를 나타내기 위해 NaN(숫자가 아님) 값을 써요. NaN은 특이한 비교 의미론을 가지기 때문에, 데이터를 정렬하거나 발생 횟수를 세는 통계 함수들에서 놀라운 또는 정의되지 않은 동작을 일으켜요. 영향을 받는 함수는 median(), median_low(), median_high(), median_grouped(), mode(), multimode(), quantiles()예요. 이 함수들을 호출하기 전에 NaN 값을 먼저 제거해야 해요:

>>> from statistics import median
>>> from math import isnan
>>> from itertools import filterfalse

>>> data = [20.7, float('NaN'),19.2, 18.3, float('NaN'), 14.4]
>>> sorted(data)  # This has surprising behavior
[20.7, nan, 14.4, 18.3, 19.2, nan]
>>> median(data)  # This result is unexpected
16.35

>>> sum(map(isnan, data))    # Number of missing values
2
>>> clean = list(filterfalse(isnan, data))  # Strip NaN values
>>> clean
[20.7, 19.2, 18.3, 14.4]
>>> sorted(clean)  # Sorting now works as expected
[14.4, 18.3, 19.2, 20.7]
>>> median(clean)       # This result is now well defined
18.75

출처: Python 표준 라이브러리

본문

평균과 중심 위치 척도

이 함수들은 모집단(population)이나 표본(sample)에서 평균 또는 대표값을 계산해요.

  • mean() — 데이터의 산술 평균("average").
  • fmean() — 빠른 부동소수점 산술 평균, 선택적 가중치 지원.
  • geometric_mean() — 데이터의 기하 평균(geometric mean).
  • harmonic_mean() — 데이터의 조화 평균(harmonic mean).
  • kde() — 데이터의 확률 밀도 분포를 추정.
  • kde_random() — kde()가 만든 PDF에서 무작위 표본 추출.
  • median() — 데이터의 중앙값(중간값).
  • median_low() — 데이터의 낮은 중앙값.
  • median_high() — 데이터의 높은 중앙값.
  • median_grouped() — 그룹화된 데이터의 중앙값(50번째 백분위수).
  • mode() — 이산 또는 명목 데이터의 단일 최빈값(가장 흔한 값).
  • multimode() — 이산 또는 명목 데이터의 최빈값 목록.
  • quantiles() — 데이터를 같은 확률의 구간들로 나누기.

퍼짐(spread) 측도

이 함수들은 모집단이나 표본이 대표값이나 평균에서 얼마나 벗어나는지의 측도를 계산해요.

  • pstdev() — 데이터의 모집단 표준편차.
  • pvariance() — 데이터의 모집단 분산.
  • stdev() — 데이터의 표본 표준편차.
  • variance() — 데이터의 표본 분산.

두 입력 사이의 관계에 대한 통계

이 함수들은 두 입력 사이의 관계에 대한 통계를 계산해요.

  • covariance() — 두 변수의 표본 공분산.
  • correlation() — Pearson과 Spearman의 상관 계수.
  • linear_regression() — 단순 선형 회귀의 기울기와 절편.

함수 상세

참고 — 함수들은 주어진 데이터가 정렬되어 있을 것을 요구하지 않아요. 다만 읽기 편하게 대부분의 예제는 정렬된 시퀀스를 보여줘요.

statistics.mean(data) — 시퀀스나 이터러블일 수 있는 data의 표본 산술 평균을 반환해요. 산술 평균은 데이터를 데이터 포인트 개수로 나눈 합이에요. 흔히 "average"라고 부르지만, 많은 수학적 평균 중 하나일 뿐이에요. 데이터의 중심 위치를 나타내는 측도예요. data가 비어 있으면 StatisticsError를 발생시켜요.

>>> mean([1, 2, 3, 4, 4])
2.8
>>> mean([-1.0, 2.5, 3.25, 5.75])
2.625

>>> from fractions import Fraction as F
>>> mean([F(3, 7), F(1, 21), F(5, 3), F(1, 3)])
Fraction(13, 21)

>>> from decimal import Decimal as D
>>> mean([D("0.5"), D("0.75"), D("0.625"), D("0.375")])
Decimal('0.5625')

참고 — 평균은 이상치(outlier)에 크게 영향받고, 데이터 포인트의 전형적인 예가 아닐 수 있어요. 더 강건하지만 덜 효율적인 중심 경향 측도가 필요하면 median()을 보세요.

표본 평균은 참 모집단 평균의 불편(unbiased) 추정량이에요. 그래서 가능한 모든 표본에 대해 평균을 내면 mean(sample)이 전체 모집단의 참 평균에 수렴해요. data가 표본이 아니라 전체 모집단을 나타내면, mean(data)는 참 모집단 평균 μ를 계산하는 것과 같아요.

statistics.fmean(data, weights=None) — 데이터를 float로 변환하고 산술 평균을 계산해요. mean() 함수보다 빠르게 실행되고 항상 float를 반환해요. 데이터는 시퀀스나 이터러블일 수 있어요. 입력 데이터셋이 비어 있으면 StatisticsError를 발생시켜요.

>>> fmean([3.5, 4.0, 5.25])
4.25

선택적 가중치를 지원해요. 예를 들어 교수가 퀴즈 20%, 과제 20%, 중간고사 30%, 기말고사 30%로 가중해서 성적을 매긴다면:

>>> grades = [85, 92, 83, 91]
>>> weights = [0.20, 0.20, 0.30, 0.30]
>>> fmean(grades, weights)
87.6

weights가 주어지면 데이터와 같은 길이여야 하고, 그렇지 않으면 ValueError가 발생해요. (버전 3.8에 추가, 버전 3.11에서 weights 지원 추가.)

statistics.geometric_mean(data) — 데이터를 float로 변환하고 기하 평균을 계산해요. 기하 평균은 값들의 곱을 사용해 데이터의 중심 경향 또는 대표값을 나타내요(산술 평균이 합을 쓰는 것과 대조적). 입력 데이터셋이 비어 있거나 0을 포함하거나 음수 값을 포함하면 StatisticsError를 발생시켜요. 데이터는 시퀀스나 이터러블일 수 있어요. 정확한 결과를 얻기 위한 특별한 노력은 하지 않아요(다만 이것은 나중에 바뀔 수 있어요).

>>> round(geometric_mean([54, 24, 36]), 1)
36.0

(버전 3.8에 추가됨.)

statistics.harmonic_mean(data, weights=None) — 실수값의 시퀀스나 이터러블인 data의 조화 평균을 반환해요. weights가 생략되거나 None이면 동일 가중치로 간주해요. 조화 평균은 데이터 역수들의 산술 mean()의 역수예요. 예를 들어 세 값 a, b, c의 조화 평균은 3/(1/a + 1/b + 1/c)와 같아요. 값 중 하나가 0이면 결과는 0이에요. 조화 평균은 평균의 한 종류로 데이터의 중심 위치 측도예요. 비율이나 비율(예: 속도)을 평균할 때 주로 적합해요.

자동차가 40 km/hr로 10km를 달리고, 다시 60 km/hr로 10km를 달린다고 해요. 평균 속도는 얼마일까요?

>>> harmonic_mean([40, 60])
48.0

자동차가 5km를 40 km/hr로 달리고, 교통이 풀린 뒤 남은 30km를 60 km/hr로 달린다면?

>>> harmonic_mean([40, 60], weights=[5, 30])
56.0

data가 비어 있거나, 어떤 원소가 0보다 작거나, 가중 합이 양수가 아니면 StatisticsError가 발생해요. 현재 알고리즘은 입력에 0을 만나면 일찍 빠져나가요. 즉 이후 입력은 유효성 검사를 받지 않아요(이 동작은 나중에 바뀔 수 있어요). (버전 3.6에 추가, 3.10에서 weights 지원 추가.)

statistics.kde(data, h, kernel='normal', *, cumulative=False) — 커널 밀도 추정(KDE)이에요. 이산 표본으로부터 연속 확률 밀도 함수 또는 누적 분포 함수를 만들어요. 기본 아이디어는 커널 함수로 데이터를 평활화해서 표본에서 모집단에 대한 추론을 돕는 거예요. 평활화 정도는 밴드폭(bandwidth)이라 불리는 스케일 파라미터 h로 제어돼요. 값이 작을수록 지역 특징을 강조하고, 클수록 더 부드러운 결과를 줘요. 커널은 표본 데이터 포인트의 상대적 가중치를 결정해요. 일반적으로 커널 모양의 선택은 더 영향력 있는 밴드폭 평활화 파라미터만큼 중요하지 않아요. 모든 표본 포인트에 어떤 가중치를 주는 커널로는 normal(gauss), logistic, sigmoid가 있어요. 밴드폭 안의 표본 포인트에만 가중치를 주는 커널로는 rectangular(uniform), triangular, parabolic(epanechnikov), quartic(biweight), triweight, cosine이 있어요. cumulative가 참이면 누적 분포 함수를 반환해요. 데이터 시퀀스가 비어 있으면 StatisticsError가 발생해요.

Wikipedia에 작은 표본에서 추정한 확률 밀도 함수를 kde()로 생성·플롯하는 예제가 있어요:

>>> sample = [-2.1, -1.3, -0.4, 1.9, 5.1, 6.2]
>>> f_hat = kde(sample, h=1.5)
>>> xarr = [i/100 for i in range(-750, 1100)]
>>> yarr = [f_hat(x) for x in xarr]

xarryarr의 점들로 PDF 플롯을 만들 수 있어요. (버전 3.13에 추가됨.)

statistics.kde_random(data, h, kernel='normal', *, seed=None)kde(data, h, kernel)이 만든 추정 확률 밀도 함수에서 무작위 선택을 하는 함수를 반환해요. seed를 주면 재현 가능한 선택이 돼요. 미래에는 더 정확한 커널 역 CDF 추정이 구현되면서 값이 약간 바뀔 수 있어요. seed는 정수, float, str, bytes일 수 있어요. 데이터 시퀀스가 비어 있으면 StatisticsError가 발생해요.

kde() 예제를 이어서, kde_random()으로 추정 확률 밀도 함수에서 새 무작위 선택을 만들 수 있어요:

>>> data = [-2.1, -1.3, -0.4, 1.9, 5.1, 6.2]
>>> rand = kde_random(data, h=1.5, seed=8675309)
>>> new_selections = [rand() for i in range(10)]
>>> [round(x, 1) for x in new_selections]
[0.7, 6.2, 1.2, 6.9, 7.0, 1.8, 2.5, -0.5, -1.8, 5.6]

(버전 3.13에 추가됨.)

statistics.median(data) — 흔한 "중간 두 값의 평균" 방법으로 숫자 데이터의 중앙값(중간값)을 반환해요. data가 비어 있으면 StatisticsError가 발생해요. data는 시퀀스나 이터러블일 수 있어요. 중앙값은 강건한 중심 위치 측도로 이상치의 존재에 덜 영향받아요. 데이터 포인트 수가 홀수면 중간 데이터 포인트를 반환해요:

>>> median([1, 3, 5])
3

짝수면 두 중간 값의 평균을 내서 중앙값을 보간해요:

>>> median([1, 3, 5, 7])
4.0

이것은 데이터가 이산적이고, 중앙값이 실제 데이터 포인트가 아니어도 괜찮을 때 적합해요. 데이터가 순서형(순서 연산 지원)이지만 수치형(덧셈 미지원)이라면 median_low()median_high()를 쓰는 걸 고려하세요.

statistics.median_low(data) — 숫자 데이터의 낮은 중앙값을 반환해요. data가 비어 있으면 StatisticsError가 발생해요. 낮은 중앙값은 항상 데이터 집합의 구성원이에요. 데이터 포인트 수가 홀수면 중간 값을, 짝수면 두 중간 값 중 작은 쪽을 반환해요.

>>> median_low([1, 3, 5])
3
>>> median_low([1, 3, 5, 7])
3

데이터가 이산적이고 중앙값이 보간된 값보다 실제 데이터 포인트이길 선호한다면 낮은 중앙값을 쓰세요.

statistics.median_high(data) — 데이터의 높은 중앙값을 반환해요. data가 비어 있으면 StatisticsError가 발생해요. 높은 중앙값은 항상 데이터 집합의 구성원이에요. 데이터 포인트 수가 홀수면 중간 값을, 짝수면 두 중간 값 중 큰 쪽을 반환해요.

>>> median_high([1, 3, 5])
3
>>> median_high([1, 3, 5, 7])
5

statistics.median_grouped(data, interval=1.0) — 연속된 고정 폭 구간들의 중간점(midpoint) 주변으로 그룹화 또는 구간화된 숫자 데이터의 중앙값을 추정해요. 데이터는 각 값이 정확히 한 구간의 중간점인 숫자 데이터의 어떤 이터러블이든 될 수 있고, 적어도 하나의 값이 있어야 해요. interval은 각 구간의 폭이에요.

예를 들어 인구 통계 정보가 연속된 10년 연령 그룹으로 요약되어 있고, 각 그룹이 구간의 5년 중간점으로 표현되어 있다고 해요:

>>> from collections import Counter
>>> demographics = Counter({
...    25: 172,   # 20 to 30 years old
...    35: 484,   # 30 to 40 years old
...    45: 387,   # 40 to 50 years old
...    55:  22,   # 50 to 60 years old
...    65:   6,   # 60 to 70 years old
... })
...

50번째 백분위수(중앙값)는 1071명 코호트 중 536번째 사람이에요. 그 사람은 3040세 연령 그룹에 있어요. 일반 median() 함수는 30대 연령 그룹의 모든 사람이 정확히 35세라고 가정할 거예요. 더 설득력 있는 가정은 그 그룹의 484명이 3040세 사이에 고르게 분포한다는 거예요. 그래서 median_grouped()를 써요:

>>> data = list(demographics.elements())
>>> median(data)
35
>>> round(median_grouped(data, interval=10), 1)
37.5

데이터 포인트가 interval의 정확한 배수로 분리되어 있는지 확인하는 것은 호출자의 책임이에요. 올바른 결과를 얻는 데 필수적이죠. 함수는 이 전제 조건을 확인하지 않아요. 입력은 보간 단계에서 float로 강제 변환될 수 있는 어떤 수치 타입이든 될 수 있어요.

statistics.mode(data) — 이산 또는 명목 데이터에서 가장 흔한 단일 데이터 포인트를 반환해요. 최빈값은 (존재할 때) 가장 전형적인 값으로 중심 위치 측도 역할을 해요. 같은 빈도의 최빈값이 여러 개 있으면 데이터에서 처음 만난 것을 반환해요. 그중 가장 작거나 큰 것을 원하면 min(multimode(data))max(multimode(data))를 쓰세요. 입력 데이터가 비어 있으면 StatisticsError가 발생해요.

mode는 이산 데이터를 가정하고 단일 값을 반환해요. 학교에서 흔히 가르치는 표준적인 최빈값 처리 방식이에요:

>>> mode([1, 1, 2, 3, 3, 3, 3, 4])
3

최빈값은 이 패키지에서 명목(비수치) 데이터에도 적용되는 유일한 통계라는 점이 특징이에요:

>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

해시 가능한 입력만 지원돼요. set 타입을 다루려면 frozenset으로 캐스팅하는 걸 고려하세요. list 타입은 tuple로 캐스팅하세요. 혼합 또는 중첩 입력에는 동등성 테스트만 의존하는 이 느린 이차 알고리즘을 쓸 수 있어요: max(data, key=data.count).

버전 3.8에서 변경: 다중 모드 데이터셋을 처리해 처음 만난 모드를 반환함. 이전에는 둘 이상의 모드를 찾으면 StatisticsError를 발생시켰어요.

statistics.multimode(data) — 데이터에서 처음 만난 순서대로 가장 자주 발생하는 값들의 목록을 반환해요. 여러 모드가 있으면 둘 이상의 결과를, 데이터가 비어 있으면 빈 목록을 반환해요:

>>> multimode('aabbbbccddddeeffffgg')
['b', 'd', 'f']
>>> multimode('')
[]

(버전 3.8에 추가됨.)

statistics.pstdev(data, mu=None) — 모집단 표준편차(모집단 분산의 제곱근)를 반환해요. 인자와 기타 세부사항은 pvariance()를 보세요.

>>> pstdev([1.5, 2.5, 2.5, 2.75, 3.25, 4.75])
0.986893273527251

statistics.pvariance(data, mu=None) — 실수값의 비어 있지 않은 시퀀스나 이터러블인 data의 모집단 분산을 반환해요. 분산(또는 평균에 대한 2차 모멘트)은 데이터의 변동성(퍼짐 또는 분산 정도)의 측도예요. 분산이 크면 데이터가 넓게 퍼져 있음을, 작으면 평균 주위에 밀집해 있음을 나타내요. 선택적 두 번째 인자 mu가 주어지면 데이터의 모집단 평균이어야 해요. 평균이 아닌 점에 대한 2차 모멘트를 계산하는 데도 쓸 수 있어요. 없거나 None(기본값)이면 산술 평균이 자동 계산돼요. 전체 모집단에서 분산을 계산할 때 이 함수를 쓰세요. 표본에서 분산을 추정하려면 보통 variance() 함수가 더 좋은 선택이에요. data가 비어 있으면 StatisticsError를 발생시켜요.

>>> data = [0.0, 0.25, 0.25, 1.25, 1.5, 1.75, 2.75, 3.25]
>>> pvariance(data)
1.25

이미 데이터의 평균을 계산했다면 선택적 두 번째 인자 mu로 전달해 재계산을 피할 수 있어요:

>>> mu = mean(data)
>>> pvariance(data, mu)
1.25

Decimal과 Fraction이 지원돼요:

>>> from decimal import Decimal as D
>>> pvariance([D("27.5"), D("30.25"), D("30.25"), D("34.5"), D("41.75")])
Decimal('24.815')

>>> from fractions import Fraction as F
>>> pvariance([F(1, 4), F(5, 4), F(1, 2)])
Fraction(13, 72)

참고 — 전체 모집단으로 호출하면 모집단 분산 σ²이 돼요. 표본에 대해 호출하면 편향된 표본 분산 s², 즉 N 자유도의 분산이 돼요. 참 모집단 평균 μ를 안다면 이 함수로 표본의 분산을 계산할 수 있어요(알려진 모집단 평균을 두 번째 인자로). 데이터 포인트가 모집단의 무작위 표본이라면 결과는 모집단 분산의 불편 추정량이 돼요.

statistics.stdev(data, xbar=None) — 표본 표준편차(표본 분산의 제곱근)를 반환해요. 인자와 기타 세부사항은 variance()를 보세요.

>>> stdev([1.5, 2.5, 2.5, 2.75, 3.25, 4.75])
1.0810874155219827

statistics.variance(data, xbar=None) — 적어도 두 개의 실수값으로 된 이터러블인 data의 표본 분산을 반환해요. 분산(또는 평균에 대한 2차 모멘트)은 데이터의 변동성의 측도예요. 선택적 두 번째 인자 xbar가 주어지면 data의 표본 평균이어야 해요. 없거나 None(기본값)이면 평균이 자동 계산돼요. 데이터가 모집단에서 가져온 표본일 때 이 함수를 쓰세요. 전체 모집단에서 분산을 계산하려면 pvariance()를 보세요. data에 값이 두 개 미만이면 StatisticsError를 발생시켜요.

>>> data = [2.75, 1.75, 1.25, 0.25, 0.5, 1.25, 3.5]
>>> variance(data)
1.3720238095238095

표본 평균을 이미 계산했다면 선택적 두 번째 인자 xbar로 전달해 재계산을 피할 수 있어요:

>>> m = mean(data)
>>> variance(data, m)
1.3720238095238095

이 함수는 xbar로 실제 평균을 전달했는지 검증하려 하지 않아요. xbar에 임의의 값을 쓰면 유효하지 않거나 불가능한 결과가 나올 수 있어요.

Decimal과 Fraction 값이 지원돼요:

>>> from decimal import Decimal as D
>>> variance([D("27.5"), D("30.25"), D("30.25"), D("34.5"), D("41.75")])
Decimal('31.01875')

>>> from fractions import Fraction as F
>>> variance([F(1, 6), F(1, 2), F(5, 3)])
Fraction(67, 108)

참고 — 이것은 베셀 보정(Bessel's correction)이 있는 표본 분산 s², 즉 N-1 자유도의 분산이에요. 데이터 포인트가 대표적이라면(예: 독립·동일 분포) 결과는 참 모집단 분산의 불편 추정량이 돼야 해요. 실제 모집단 평균 μ를 안다면 pvariance() 함수에 mu 파라미터로 전달해 표본의 분산을 얻을 수 있어요.

statistics.quantiles(data, *, n=4, method='exclusive')data를 같은 확률의 n개 연속 구간으로 나눠요. 구간을 나누는 n - 1개 절단점 목록을 반환해요. n을 4로 하면 사분위수(기본값), 10으로 하면 십분위수, 100으로 하면 데이터를 100개의 같은 크기 그룹으로 나누는 99개 절단점을 주는 백분위수가 돼요. n이 1보다 작으면 StatisticsError를 발생시켜요. 데이터는 표본 데이터를 담은 어떤 이터러블이든 될 수 있어요. 의미 있는 결과를 얻으려면 data의 데이터 포인트 수가 n보다 커야 해요. 데이터 포인트가 하나도 없으면 StatisticsError를 발생시켜요.

절단점은 가장 가까운 두 데이터 포인트에서 선형 보간돼요. 예를 들어 절단점이 두 표본 값 100112 사이 1/3 지점에 떨어지면, 절단점은 104로 평가돼요.

분위수 계산 방법은 데이터가 모집단의 최저·최고 가능값을 포함하는지 제외하는지에 따라 달라질 수 있어요. 기본 방법은 "exclusive"로, 표본보다 더 극단적인 값이 있을 수 있는 모집단에서 샘플링한 데이터에 쓰여요. 정렬된 m개 데이터 포인트 중 i번째 아래로 떨어지는 모집단의 비율은 i / (m + 1)로 계산돼요. 9개의 표본 값이 주어지면 정렬해 다음 백분위수를 할당해요: 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%.

method를 "inclusive"로 설정하면 모집단 데이터를 설명하거나 모집단의 가장 극단적인 값을 포함하는 것으로 알려진 표본에 쓰여요. data의 최솟값은 0번째 백분위수로, 최댓값은 100번째 백분위수로 취급돼요. 정렬된 m개 데이터 포인트 중 i번째 아래로 떨어지는 모집단의 비율은 (i - 1) / (m - 1)로 계산돼요. 11개의 표본 값이 주어지면 0%, 10%, …, 100%를 할당해요.

# Decile cut points for empirically sampled data
>>> data = [105, 129, 87, 86, 111, 111, 89, 81, 108, 92, 110,
...         100, 75, 105, 103, 109, 76, 119, 99, 91, 103, 129,
...         106, 101, 84, 111, 74, 87, 86, 103, 103, 106, 86,
...         111, 75, 87, 102, 121, 111, 88, 89, 101, 106, 95,
...         103, 107, 101, 81, 109, 104]
>>> [round(q, 1) for q in quantiles(data, n=10)]
[81.0, 86.2, 89.0, 99.4, 102.5, 103.6, 106.0, 109.8, 111.0]

(버전 3.8에 추가됨. 버전 3.13에서 변경: 단 하나의 데이터 포인트만 있는 입력에 대해 더 이상 예외를 발생시키지 않음. 이를 통해 분위수 추정을 표본 포인트를 하나씩 쌓아가며 각 새 데이터 포인트마다 점점 정교해지게 만들 수 있어요.)

statistics.covariance(x, y, /) — 두 입력 xy의 표본 공분산을 반환해요. 공분산은 두 입력의 결합 변동성의 측도예요. 두 입력 모두 같은 길이여야 해요(2개 이상). 그렇지 않으면 StatisticsError가 발생해요.

>>> x = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> y = [1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> covariance(x, y)
0.75
>>> z = [9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> covariance(x, z)
-7.5
>>> covariance(z, x)
-7.5

(버전 3.10에 추가됨.)

statistics.correlation(x, y, /, *, method='linear') — 두 입력에 대한 Pearson 상관 계수를 반환해요. Pearson 상관 계수 r은 -1과 +1 사이의 값을 가져요. 선형 관계의 강도와 방향을 측정해요. method가 "ranked"면 두 입력에 대한 Spearman 순위 상관 계수를 계산해요. 데이터는 순위로 대체되고, 동률(ties)은 평균내서 같은 값이 같은 순위를 받아요. 결과 계수는 단조 관계의 강도를 측정해요. Spearman 상관 계수는 순서형 데이터나 Pearson 상관 계수의 선형 비례 요건을 충족하지 못하는 연속 데이터에 적합해요. 두 입력 모두 같은 길이여야 하고(2개 이상) 상수여선 안 돼요. 그렇지 않으면 StatisticsError가 발생해요.

케플러의 행성 운동 법칙을 예로 들어요:

>>> # Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and  Neptune
>>> orbital_period = [88, 225, 365, 687, 4331, 10_756, 30_687, 60_190]    # days
>>> dist_from_sun = [58, 108, 150, 228, 778, 1_400, 2_900, 4_500] # million km

>>> # Show that a perfect monotonic relationship exists
>>> correlation(orbital_period, dist_from_sun, method='ranked')
1.0

>>> # Observe that a linear relationship is imperfect
>>> round(correlation(orbital_period, dist_from_sun), 4)
0.9882

>>> # Demonstrate Kepler's third law: There is a linear correlation
>>> # between the square of the orbital period and the cube of the
>>> # distance from the sun.
>>> period_squared = [p * p for p in orbital_period]
>>> dist_cubed = [d * d * d for d in dist_from_sun]
>>> round(correlation(period_squared, dist_cubed), 4)
1.0

(버전 3.10에 추가됨. 버전 3.12에서 변경: Spearman 순위 상관 계수 지원 추가.)

statistics.linear_regression(x, y, /, *, proportional=False) — 최소제곱법(ordinary least squares)으로 추정한 단순 선형 회귀 파라미터의 기울기(slope)와 절편(intercept)을 반환해요. 단순 선형 회귀는 독립 변수 x와 종속 변수 y의 관계를 이 선형 함수로 설명해요:

y = slope * x + intercept + noise

여기서 slopeintercept는 추정되는 회귀 파라미터이고, noise는 선형 회귀로 설명되지 않은 데이터의 변동성을 나타내요(종속 변수의 예측값과 실제값의 차이와 같아요). 두 입력 모두 같은 길이여야 하고(2개 이상), 독립 변수 x는 상수일 수 없어요. 그렇지 않으면 StatisticsError가 발생해요.

예를 들어 Monty Python 영화들의 개봉 연도를 써서, 그 페이스를 유지했다면 2019년까지 제작되었을 Monty Python 영화의 누적 수를 예측할 수 있어요.

>>> year = [1971, 1975, 1979, 1982, 1983]
>>> films_total = [1, 2, 3, 4, 5]
>>> slope, intercept = linear_regression(year, films_total)
>>> round(slope * 2019 + intercept)
16

proportional이 참이면 독립 변수 x와 종속 변수 y가 직접 비례한다고 가정해요. 데이터는 원점을 지나는 직선에 맞춰져요. 절편이 항상 0.0이므로 밑에 깔린 선형 함수는 다음과 같이 단순해져요:

y = slope * x + noise

correlation()의 예제를 이어서, 주요 행성에 기반한 모델이 왜행성의 궤도 거리를 얼마나 잘 예측하는지 봐요:

>>> model = linear_regression(period_squared, dist_cubed, proportional=True)
>>> slope = model.slope

>>> # Dwarf planets:   Pluto,  Eris,    Makemake, Haumea, Ceres
>>> orbital_periods = [90_560, 204_199, 111_845, 103_410, 1_680]  # days
>>> predicted_dist = [math.cbrt(slope * (p * p)) for p in orbital_periods]
>>> list(map(round, predicted_dist))
[5912, 10166, 6806, 6459, 414]

>>> [5_906, 10_152, 6_796, 6_450, 414]  # actual distance in million km
[5906, 10152, 6796, 6450, 414]

(버전 3.10에 추가됨. 버전 3.11에서 변경: proportional 지원 추가.)

예외

exception statistics.StatisticsError — 통계 관련 예외를 위한 ValueError의 하위 클래스예요.

NormalDist 객체

NormalDist는 무작위 변수의 정규 분포를 만들고 조작하는 도구예요. 데이터 측정의 평균과 표준편차를 하나의 개체로 다루는 클래스죠. 정규 분포는 중심극한정리에서 발생하며 통계에서 널리 응용돼요.

class statistics.NormalDist(mu=0.0, sigma=1.0)mu가 산술 평균, sigma가 표준편차를 나타내는 새 NormalDist 객체를 반환해요. sigma가 음수면 StatisticsError를 발생시켜요.

  • mean — 정규 분포의 산술 평균에 대한 읽기 전용 프로퍼티.
  • median — 정규 분포의 중앙값에 대한 읽기 전용 프로퍼티.
  • mode — 정규 분포의 최빈값에 대한 읽기 전용 프로퍼티.
  • stdev — 정규 분포의 표준편차에 대한 읽기 전용 프로퍼티.
  • variance — 정규 분포의 분산에 대한 읽기 전용 프로퍼티. 표준편차의 제곱과 같아요.
  • classmethod from_samples(data)fmean()stdev()로 데이터에서 추정한 musigma 파라미터로 정규 분포 인스턴스를 만들어요. 데이터는 어떤 이터러블이든 될 수 있고 float 타입으로 변환될 수 있는 값들로 구성되어야 해요. 데이터에 두 개 미만의 요소가 있으면 StatisticsError를 발생시켜요. 중심 값을 추정하려면 적어도 한 포인트, 퍼짐을 추정하려면 적어도 두 포인트가 필요하니까요.
  • samples(n, *, seed=None) — 주어진 평균과 표준편차에 대해 n개의 무작위 표본을 생성해요. float 값의 list를 반환해요. seed가 주어지면 밑에 깔린 난수 생성기의 새 인스턴스를 만들어요. 이는 멀티스레딩 상황에서도 재현 가능한 결과를 만드는 데 유용해요. 버전 3.13에서 변경: 더 빠른 알고리즘으로 전환됨. 이전 버전의 표본을 재현하려면 random.seed()random.gauss()를 쓰세요.
  • pdf(x) — 확률 밀도 함수(pdf)를 사용해 무작위 변수 X가 주어진 값 x 근처에 있을 상대적 가능성을 계산해요. 수학적으로는 dx가 0에 접근할 때 P(x <= X < x+dx) / dx 비율의 극한이에요. 상대적 가능성은 좁은 범위에 표본이 발생할 확률을 그 범위의 폭으로 나눈 값으로 계산돼요(그래서 "밀도"라는 단어). 가능성은 다른 점에 상대적이므로 값이 1.0보다 클 수 있어요.
  • cdf(x) — 누적 분포 함수(cdf)를 사용해 무작위 변수 X가 x보다 작거나 같을 확률을 계산해요. 수학적으로 P(X <= x)로 쓰여요.
  • inv_cdf(p) — 역 누적 분포 함수(분위수 함수 또는 percent-point 함수라고도 함)를 계산해요. 수학적으로 x : P(X <= x) = p로 쓰여요. 변수가 그 값보다 작거나 같을 확률이 주어진 확률 p와 같아지는 무작위 변수 X의 값 x를 찾아요.
  • overlap(other) — 두 정규 확률 분포 사이의 일치 정도를 측정해요. 두 확률 밀도 함수의 겹치는 면적을 주는 0.0~1.0 사이 값을 반환해요.
  • quantiles(n=4) — 정규 분포를 같은 확률의 n개 연속 구간으로 나눠요. 구간을 나누는 (n - 1)개 절단점 목록을 반환해요. n을 4로 하면 사분위수(기본값), 10으로 하면 십분위수, 100으로 하면 정규 분포를 100개의 같은 크기 그룹으로 나누는 99개 절단점을 주는 백분위수가 돼요.
  • zscore(x) — 정규 분포의 평균 위 또는 아래의 표준편차 수로 x를 설명하는 표준 점수(Standard Score)를 계산해요: (x - mean) / stdev. (버전 3.9에 추가됨.)

NormalDist 인스턴스는 상수에 의한 덧셈·뺄셈·곱셈·나눗셈을 지원해요. 이 연산들은 평행 이동과 스케일링에 쓰여요. 예를 들어:

>>> temperature_february = NormalDist(5, 2.5)             # Celsius
>>> temperature_february * (9/5) + 32                     # Fahrenheit
NormalDist(mu=41.0, sigma=4.5)

상수를 NormalDist 인스턴스로 나누는 것은 지원되지 않아요. 결과가 정규 분포가 아니기 때문이에요. 정규 분포는 독립 변수들의 가산적 효과에서 생기므로, NormalDist 인스턴스로 표현된 두 개의 독립적으로 정규 분포한 무작위 변수를 더하고 뺄 수 있어요. 예를 들어:

>>> birth_weights = NormalDist.from_samples([2.5, 3.1, 2.1, 2.4, 2.7, 3.5])
>>> drug_effects = NormalDist(0.4, 0.15)
>>> combined = birth_weights + drug_effects
>>> round(combined.mean, 1)
3.1
>>> round(combined.stdev, 1)
0.5

(버전 3.8에 추가됨.)

예제와 레시피

고전 확률 문제

NormalDist는 고전 확률 문제를 쉽게 풀어요. 예를 들어 SAT 시험의 역사적 데이터에 따르면 점수가 평균 1060, 표준편차 195인 정규 분포를 따른다고 해요. 반올림한 정수로, 시험 점수가 1100과 1200 사이인 학생의 비율을 구해 볼게요:

>>> sat = NormalDist(1060, 195)
>>> fraction = sat.cdf(1200 + 0.5) - sat.cdf(1100 - 0.5)
>>> round(fraction * 100.0, 1)
18.4

SAT 점수의 사분위수와 십분위수를 찾아요:

>>> list(map(round, sat.quantiles()))
[928, 1060, 1192]
>>> list(map(round, sat.quantiles(n=10)))
[810, 896, 958, 1011, 1060, 1109, 1162, 1224, 1310]

시뮬레이션용 Monte Carlo 입력

해석적으로 풀기 어려운 모델의 분포를 추정하려면 NormalDist가 Monte Carlo 시뮬레이션용 입력 표본을 생성할 수 있어요:

>>> def model(x, y, z):
...     return (3*x + 7*x*y - 5*y) / (11 * z)
...
>>> n = 100_000
>>> X = NormalDist(10, 2.5).samples(n, seed=3652260728)
>>> Y = NormalDist(15, 1.75).samples(n, seed=4582495471)
>>> Z = NormalDist(50, 1.25).samples(n, seed=6582483453)
>>> quantiles(map(model, X, Y, Z))
[1.4591308524824727, 1.8035946855390597, 2.175091447274739]

이항 분포 근사

정규 분포는 표본 크기가 크고 성공 확률이 50% 근처일 때 이항 분포를 근사하는 데 쓸 수 있어요. 예를 들어 오픈소스 컨퍼런스에 750명이 참석하고, 각각 500명 수용 가능한 두 방이 있다고 해요. Python 얘기와 Ruby 얘기가 하나씩 있죠. 이전 컨퍼런스에서는 참석자의 65%가 Python 얘기를 선호했어요. 인구 선호도가 바뀌지 않았다고 가정하면, Python 방이 수용 한도를 넘지 않을 확률은?

>>> n = 750             # Sample size
>>> p = 0.65            # Preference for Python
>>> q = 1.0 - p         # Preference for Ruby
>>> k = 500             # Room capacity

>>> # Approximation using the cumulative normal distribution
>>> from math import sqrt
>>> round(NormalDist(mu=n*p, sigma=sqrt(n*p*q)).cdf(k + 0.5), 4)
0.8402

>>> # Exact solution using the cumulative binomial distribution
>>> from math import comb, fsum
>>> round(fsum(comb(n, r) * p**r * q**(n-r) for r in range(k+1)), 4)
0.8402

>>> # Approximation using a simulation
>>> from random import seed, binomialvariate
>>> seed(8675309)
>>> mean(binomialvariate(n, p) <= k for i in range(10_000))
0.8406

나이브 베이즈 분류기

정규 분포는 머신러닝 문제에서 흔히 나타나요. Wikipedia에 나이브 베이즈 분류기의 좋은 예가 있어요. 키, 몸무게, 발 크기 같은 정규 분포한 특징들을 측정해 사람의 성별을 예측하는 문제예요. 여덟 명에 대한 측정값이 담긴 훈련 데이터셋이 주어졌어요. 측정값이 정규 분포한다고 가정하므로 데이터를 NormalDist로 요약해요:

>>> height_male = NormalDist.from_samples([6, 5.92, 5.58, 5.92])
>>> height_female = NormalDist.from_samples([5, 5.5, 5.42, 5.75])
>>> weight_male = NormalDist.from_samples([180, 190, 170, 165])
>>> weight_female = NormalDist.from_samples([100, 150, 130, 150])
>>> foot_size_male = NormalDist.from_samples([12, 11, 12, 10])
>>> foot_size_female = NormalDist.from_samples([6, 8, 7, 9])

다음으로, 특징 측정값은 알지만 성별은 모르는 새 사람을 만나요:

>>> ht = 6.0        # height
>>> wt = 130        # weight
>>> fs = 8          # foot size

남성 또는 여성일 선험(prior) 확률 50%에서 시작해, 성별이 주어졌을 때 특징 측정값의 우도(likelihood)들의 곱에 선험을 곱해 사후(posterior)를 계산해요:

>>> prior_male = 0.5
>>> prior_female = 0.5
>>> posterior_male = (prior_male * height_male.pdf(ht) *
...                   weight_male.pdf(wt) * foot_size_male.pdf(fs))

>>> posterior_female = (prior_female * height_female.pdf(ht) *
...                     weight_female.pdf(wt) * foot_size_female.pdf(fs))

최종 예측은 가장 큰 사후로 가요. 이것을 최대 사후 추정(MAP)이라고 해요:

>>> 'male' if posterior_male > posterior_female else 'female'
'female'