[파이썬 100강] 63강. statistics로 평균·중앙값·산포를 빠르게 읽기
데이터를 보면 가장 먼저 나오는 질문은 보통 비슷합니다. “평균이 얼마야?”, “이상치 때문에 왜곡된 건 아니야?”, “흩어짐은 어느 정도야?” 같은 질문이죠. statistics 모듈은 이 기본 질문에 바로 답할 수 있게 해주는 표준 라이브러리입니다. 서론은 짧게 하고, 바로 손에 잡히는 예제로 들어가겠습니다.
핵심 개념
mean,median,mode는 중심 경향(대표값)을 파악하는 가장 기본 도구다.pstdev,stdev,variance는 데이터가 얼마나 퍼져 있는지(산포)를 수치로 보여준다.- 이상치(outlier)가 있으면 평균보다 중앙값이 더 안정적인 의사결정 지표가 될 수 있다.
- 표본(sample)과 모집단(population) 함수를 구분하지 않으면 리포트 수치가 달라질 수 있다.
- 숫자 요약은 “결론”이 아니라 “신호”다. 도메인 맥락과 함께 읽어야 실수가 줄어든다.
statistics를 잘 쓴다는 건 단순히 함수를 외우는 일이 아닙니다. 핵심은 “무슨 질문에 어떤 수치를 붙일지”를 분명히 하는 것입니다. 예를 들어 CS 응답시간을 볼 때, 평균만 보면 장애 한 번의 긴 지연 때문에 팀이 과도하게 불안해질 수 있습니다. 반대로 중앙값만 보면 드물지만 치명적인 꼬리 구간을 놓치기도 합니다. 그래서 실무에서는 평균/중앙값/표준편차를 함께 보고, 필요하면 최소·최대나 백분위까지 확장하는 흐름이 일반적입니다. 또한 표본 표준편차(stdev)와 모집단 표준편차(pstdev)를 섞어 쓰면 대시보드 숫자가 팀마다 달라지는 문제가 생기므로, 지표 정의를 문서로 고정하는 습관이 중요합니다.
기본 사용
예제 1) 평균·중앙값·최빈값 한 번에 보기
>>> import statistics as stats
>>> values = [72, 75, 78, 80, 82, 90, 90]
>>> stats.mean(values)
81
>>> stats.median(values)
80
>>> stats.mode(values)
90
해설:
- 평균은 전체 수준을 빠르게 보여줍니다.
- 중앙값은 정렬 기준 가운데 값이라서 일부 극단값의 영향이 작습니다.
- 최빈값은 “가장 자주 나타난 값”을 알려줘서 사용자 행동 분석, 상품 사이즈 선택 같은 빈도 중심 문제에 유용합니다.
예제 2) 이상치가 있을 때 평균 vs 중앙값
>>> import statistics as stats
>>> normal_day = [120, 118, 125, 130, 127, 123, 121]
>>> incident_day = [120, 118, 125, 130, 127, 123, 900]
>>> stats.mean(normal_day), stats.median(normal_day)
(123.42857142857143, 123)
>>> stats.mean(incident_day), stats.median(incident_day)
(234.71428571428572, 125)
해설:
- 장애로 900ms 같은 값이 하나 들어오자 평균이 크게 튑니다.
- 중앙값은 상대적으로 안정적이라 “평소 체감 성능”을 해석할 때 더 현실적인 지표가 될 수 있습니다.
- 그래서 운영 리포트는 평균 하나로 끝내지 않고, 중앙값을 같이 적는 것이 안전합니다.
예제 3) 표본/모집단 표준편차 구분
>>> import statistics as stats
>>> sample = [48, 50, 52, 49, 51]
>>> stats.pstdev(sample)
1.4142135623730951
>>> stats.stdev(sample)
1.5811388300841898
>>> stats.pvariance(sample), stats.variance(sample)
(2, 2.5)
해설:
pstdev/pvariance는 모집단 전체를 이미 갖고 있다고 가정합니다.stdev/variance는 모집단 일부(표본)만 관측했다고 가정합니다.- 데이터가 같은데 값이 달라지는 이유가 여기에 있으므로, 팀 문서에 어떤 정의를 쓰는지 못 박아야 합니다.
예제 4) 실무형 미니 리포트 함수
>>> import statistics as stats
>>> def summarize_latency(latencies):
... return {
... "count": len(latencies),
... "mean": round(stats.mean(latencies), 2),
... "median": round(stats.median(latencies), 2),
... "pstdev": round(stats.pstdev(latencies), 2),
... "min": min(latencies),
... "max": max(latencies),
... }
...
>>> summarize_latency([101, 99, 110, 95, 108, 300])
{'count': 6, 'mean': 135.5, 'median': 104.5, 'pstdev': 73.2, 'min': 95, 'max': 300}
해설:
- 실무에서는 값을 “한 번 계산하고 여러 곳에서 재사용”하도록 함수화하면 좋습니다.
round를 함수 내부에서 통일하면 대시보드/문서/알림 숫자 자리수가 일관됩니다.min/max를 함께 두면 산포가 커진 상황을 직관적으로 파악할 수 있습니다.
자주 하는 실수
실수 1) 빈 리스트를 그대로 넣어 예외 발생
>>> import statistics as stats
>>> stats.mean([])
Traceback (most recent call last):
...
statistics.StatisticsError: mean requires at least one data point
원인:
- 수집 파이프라인에서 필터링 후 데이터가 0건이 되는 경우를 고려하지 않았습니다.
해결:
>>> import statistics as stats
>>> def safe_mean(values):
... return None if not values else stats.mean(values)
...
>>> safe_mean([])
>>> safe_mean([10, 20, 30])
20
- 요약 함수 입구에서 빈 데이터 처리 정책(
None,0, 에러 전파)을 명시하세요. - 리포트 화면에서도 “데이터 없음” 상태를 별도 표현해야 오해가 줄어듭니다.
실수 2) 표본/모집단 함수를 뒤바꿔 보고서 수치 불일치
>>> import statistics as stats
>>> values = [12, 11, 13, 10, 14]
>>> stats.stdev(values), stats.pstdev(values)
(1.5811388300841898, 1.4142135623730951)
원인:
- 팀 A는
stdev, 팀 B는pstdev를 써서 같은 지표 이름인데 값이 달라졌습니다.
해결:
- 지표 사전에 “표본 표준편차 사용” 또는 “모집단 표준편차 사용”을 명시합니다.
- 공통 유틸 함수 하나로만 계산하게 해서 구현 분기를 제거합니다.
실수 3) mode()가 항상 하나만 나온다고 가정
>>> import statistics as stats
>>> data = [1, 1, 2, 2, 3]
>>> stats.multimode(data)
[1, 2]
원인:
- 실제 데이터는 동률 최빈값이 흔한데, 단일 값만 올 거라고 가정했습니다.
해결:
>>> import statistics as stats
>>> data = [1, 1, 2, 2, 3]
>>> modes = stats.multimode(data)
>>> modes
[1, 2]
>>> ", ".join(map(str, modes))
'1, 2'
- 다봉 분포 가능성이 있으면
mode()대신multimode()를 기본으로 두세요. - 화면/리포트 포맷도 “복수 가능”을 전제로 설계하면 장애를 줄일 수 있습니다.
실무 패턴
-
입력 검증 규칙
- 숫자형이 아닌 값(
None, 공백 문자열)을 초기에 제거/치환합니다. - 단위(ms, s, 원, 달러)를 섞지 않도록 수집 단계에서 정규화합니다.
- 숫자형이 아닌 값(
-
로그/예외 처리 규칙
StatisticsError는 데이터 품질 경고로 분류하고, 시스템 장애와 분리해 알림 임계치를 다르게 둡니다.- 계산 실패 시 원본 샘플 개수와 필터링 개수를 함께 로그에 남겨 원인 추적을 쉽게 만듭니다.
-
재사용 함수/구조화 팁
summarize_*함수를 도메인별(응답시간, 주문금액, 사용량)로 나누되 반환 스키마는 통일합니다.- 대시보드·배치·알림이 같은 함수를 쓰게 하면 지표 불일치가 크게 줄어듭니다.
-
성능/메모리 체크 포인트
statistics는 in-memory 리스트 기반이므로 초대용량에는 적합하지 않을 수 있습니다.- 데이터가 매우 크면 샘플링, 구간 집계, 또는 pandas/DB 집계로 전환하는 기준을 정해두세요.
-
협업 운영 팁
- 지표 설명 문서에 “의미/계산식/예외 처리/빈 데이터 정책”을 한 페이지로 관리하세요.
- 코드 리뷰에서 “평균만 쓰지 않았는지”를 체크리스트 항목으로 넣으면 분석 품질이 좋아집니다.
오늘의 결론
한 줄 요약: statistics는 숫자를 예쁘게 만드는 도구가 아니라, 데이터를 오해 없이 읽기 위한 최소 안전장치입니다.
기억할 것:
- 평균 하나만 믿지 말고 중앙값·산포를 함께 본다.
- 표본/모집단 함수 선택 기준을 팀 규칙으로 고정한다.
- 빈 데이터와 동률 최빈값 같은 경계 상황을 먼저 설계한다.
연습문제
- 주문 처리 시간 리스트
[3.2, 3.5, 3.1, 3.4, 12.0, 3.3]에 대해 평균·중앙값·모집단 표준편차를 계산하고, 이상치(12.0)가 해석에 어떤 영향을 주는지 3문장으로 설명해 보세요. - 점수 리스트
[70, 75, 75, 80, 80, 90]에서mode()와multimode()결과를 비교하고, 리포트에 어떤 방식으로 표시할지 포맷을 제안해 보세요. - 빈 리스트, 숫자 1개만 있는 리스트, 숫자 여러 개 리스트를 입력받아 요약 딕셔너리를 반환하는
safe_summary()함수를 작성해 보세요. (mean,median,pstdev포함)
이전 강의 정답
- 문자열 분수 리스트 합이 정확히 1인지 검사
>>> from fractions import Fraction
>>> items = ['1/3', '1/6', '1/2']
>>> total = sum(Fraction(x) for x in items)
>>> total, total == 1
(Fraction(1, 1), True)
- 1200ml를 7:5:3 비율로 분배
>>> from fractions import Fraction
>>> total = 1200
>>> r = [Fraction(7, 15), Fraction(5, 15), Fraction(3, 15)]
>>> portions = [int(total * x) for x in r]
>>> portions
[560, 400, 240]
- e 근사값의 분모 제한별 오차 비교
>>> from fractions import Fraction
>>> x = Fraction('2.7182818')
>>> a = x.limit_denominator(100)
>>> b = x.limit_denominator(1000)
>>> float(abs(x - a)), float(abs(x - b))
(0.003996085106382979, 2.8181818181818182e-05)
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 주요 표준 라이브러리:
statistics - 검증 방식: REPL(pycon) 예제 수동 실행 + 게시 전 dry-run 검사