사전 중앙값
개념

중앙값

gabury1고친 사람 github-actions[bot]

중앙값은 값 여럿을 대표하는 수 하나를 골라 줍니다. 값을 크기순으로 줄 세운 다음 한가운데에 선 값을 그 대표로 씁니다. 유난히 크거나 작은 값이 몇 개 섞여 있어도 이 수는 거의 안 움직입니다.

쉽고 빠른 이해

중앙값은 값 묶음에서 한가운데 값 하나를 집어 대표로 내놓습니다. 응답 시간 열 건 가운데 아홉 건은 0.1~0.2초, 한 건은 3.7초입니다. 이때 중앙값은 0.15초로 아홉 건 쪽을 가리킵니다.

이게 없으면 값 묶음을 대표하는 수를 평균으로만 말하게 됩니다. 평균은 오래 걸린 한 건을 나머지에 섞어 0.5초로 끌어올립니다. 열 건 가운데 그만큼 걸린 요청은 하나도 없습니다.

  1. 값을 작은 것부터 큰 것까지 줄 세웁니다
  2. 줄의 한가운데를 찾습니다
  3. 건수가 홀수면 가운데 한 건의 값을, 짝수면 가운데 두 건의 평균을 씁니다

대가는 오래 걸린 쪽을 못 본다는 것입니다. 뒤쪽 절반이 어떤 모양이든 같은 수가 나옵니다. 줄을 세워야 하니 값을 전부 들고 있어야 합니다. 서버마다 낸 중앙값을 모아 다시 중앙값을 내도 전체의 중앙값이 안 나옵니다.

상세

소풍 가서 아이들을 키 순으로 세웁니다. 맨 앞과 맨 뒤만 보면 이 무리가 대체로 얼마나 큰지 안 잡힙니다. 줄의 한가운데 선 아이를 보고 「이 정도가 이 무리의 키」라고 말합니다.

중앙값은 값들을 크기순으로 늘어놓았을 때 한가운데에 오는 값입니다. 그 값보다 작은 값이 절반, 큰 값이 절반입니다. 응답 시간 다섯 건이 0.1초, 0.1초, 0.2초, 0.3초, 3.7초라면 중앙값은 세 번째인 0.2초입니다. 같은 다섯 건의 평균은 0.88초입니다.

값 여럿을 수 하나로 줄여 내놓는 이런 수를 대표값이라고 부릅니다. 중앙값과 평균은 둘 다 대표값입니다.

홀수 건과 짝수 건

계산은 두 단계입니다. 먼저 값을 작은 것부터 줄 세웁니다. 그리고 줄의 한가운데에 있는 값을 읽습니다. 그래서 결과로 나오는 것은 대개 원래 묶음 안에 있던 값 하나입니다.

건수가 홀수면 한가운데가 딱 하나입니다. N 건이면 (N+1)/2 번째 값이 중앙값입니다. 다섯 건이면 세 번째, 아홉 건이면 다섯 번째입니다.

건수가 짝수면 한가운데에 두 건이 섭니다. N/2 번째 값과 N/2+1 번째 값의 평균을 중앙값으로 씁니다. 네 건이면 두 번째와 세 번째의 평균입니다.

flowchart TD
    subgraph 홀수["다섯 건 · 가운데가 한 건"]
        A1["1"] --> A2["2"] --> A3(("7")) --> A4["9"] --> A5["30"]
    end
    subgraph 짝수["네 건 · 가운데가 두 건"]
        B1["2"] --> B2(("6")) --> B3(("10")) --> B4["40"]
    end
    홀수 --> H["세 번째 값 7 이 중앙값"]
    짝수 --> J["6 과 10 의 평균 8 이 중앙값"]

동그란 칸이 한가운데에 선 건입니다. 홀수 줄은 가운데가 한 건이라 그 값을 읽으면 끝납니다. 짝수 줄은 가운데가 두 건이라 둘을 더해 반으로 나눕니다.

짝수일 때는 결과가 원래 묶음에 없던 값이 되기도 합니다. 짝수 줄 네 건에 8 은 없지만 중앙값은 8 입니다.

극단값에 안 흔들리는 성질

중앙값은 값이 줄에서 몇 번째인지만 봅니다. 이 몇 번째를 순위라고 부릅니다. 중앙값은 순위로 값을 고르므로 줄 끝에 선 값이 얼마나 큰지는 결과에 안 들어갑니다. 이 성질을 응답 시간 열 건으로 평균과 견줘 봅니다.

줄 끝에서 나머지와 멀찍이 떨어진 값을 극단값이라고 부릅니다. 이상치라고도 부릅니다. 같은 것을 가리키는 두 이름입니다. 이런 값들이 줄의 끝에 드물게 늘어선 부분은 꼬리라고 부릅니다.

평균은 반대입니다. 값을 모두 더하므로 모든 값이 조금씩 결과에 들어갑니다. 아주 큰 값 하나가 섞이면 그 값이 결과를 끌고 올라갑니다.

응답 시간 열 건을 줄 세우면 0.1 · 0.1 · 0.1 · 0.1 · 0.15 · 0.15 · 0.2 · 0.2 · 0.2 · 3.7 초입니다. 아홉 건은 0.1초에서 0.2초 사이입니다. 한 건만 3.7초입니다.

flowchart TD
    subgraph 줄["응답 시간 열 건 · 작은 것부터"]
        L["앞 네 건"] --> M["한가운데 두 건"] --> R["뒤 세 건"] --> T["맨 뒤 한 건 · 3.7 초"]
    end
    M --> C["중앙값이 읽는 칸"]
    T --> A["평균이 끌려가는 쪽"]

중앙값은 한가운데 두 칸에서 값을 읽습니다. 맨 뒤가 아무리 커져도 읽는 칸이 안 바뀝니다. 평균은 열 건을 모두 더하므로 맨 뒤 한 건 쪽으로 끌려갑니다.

대표값 열 건에서 나온 수 맨 뒤 3.7초가 37초가 되면
중앙값 0.15초 0.15초
평균 0.5초 3.83초

꼬리의 극단값은 평균을 끌어당기지만 중앙값은 못 움직입니다.

분위수 눈금의 절반

중앙값은 줄을 절반에서 자른 경계입니다. 줄을 정해진 비율에서 자르고 그 경계의 값을 읽는 계산을 분위수라고 부릅니다. 0.5 에서 자르면 0.5 분위수입니다. 이것이 중앙값입니다.

비율을 백 칸 눈금으로 부르는 것이 백분위수입니다. 절반은 백 칸 중 쉰 번째 경계이므로 중앙값은 50번째 백분위수와 같은 값입니다.

부르는 이름 어느 눈금에서 부른 것인가
중앙값 값 줄의 한가운데
0.5 분위수 0에서 1까지 비율 눈금의 0.5
50번째 백분위수 백 칸 눈금의 쉰 번째
p50 백분위수(percentile)의 p 를 붙여 50번째 백분위수를 줄여 적은 표기

이름이 넷이지만 뜻은 하나입니다. 성능 지표에서는 p50 옆에 뒤쪽을 보는 분위수를 같이 적습니다. 0.95 분위수, 곧 95번째 백분위수를 p95 로 적은 것이 그런 예입니다. 둘을 나란히 놓으면 한가운데와 뒤쪽 끝을 같이 봅니다.

block-beta
columns 2
  L["앞 절반 · 작은 쪽"] R["뒤 절반 · 큰 쪽"]
  M["둘을 가르는 경계가 중앙값 · p50"] N["뒤 절반의 끝 가까이가 p95"]

값을 줄 세워 놓고 앞 절반과 뒤 절반으로 가른 모습입니다. p50 은 둘을 가르는 경계에 서고 p95 는 뒤 절반의 끝 가까이에 섭니다.

배경

값 여럿을 수 하나로 줄여 말해야 할 때가 많습니다. 가장 먼저 떠올리는 대표값은 값을 모두 더해 건수로 나눈 평균입니다.

값이 한쪽으로 길게 늘어진 분포에서는 평균이 늘어진 쪽으로 끌려갑니다. 큰 쪽 꼬리에 값이 더 많으면 평균이 중앙값보다 커집니다. 꼬리에 아주 큰 값이 몇 개만 있어도 평균은 대다수 값보다 한참 위에 섭니다. 이런 분포에서는 평균을 대표로 내놓아도 대다수 값이 겪지 않은 수가 대표로 섭니다.

필요했던 것은 극단값이 못 끌어당기는 대표값입니다. 값의 크기 대신 순위로 대표를 고르면 꼬리의 극단값은 줄 맨 끝의 한 건일 뿐입니다. 줄의 한가운데 값을 대표로 삼는 중앙값(median)이 이 생각입니다. 꼬리에 극단값이 있는 데이터에서는 대체로 중앙값이 평균보다 대표값을 더 잘 잡습니다.

예시

언어의 표준 라이브러리 · 데이터베이스 질의 · 배포 지표에서 중앙값이 어떻게 나오는지 봅니다. 셋 다 줄 세우기와 한가운데 읽기를 하는 함수가 따로 있습니다.

Python statistics.median

Python 표준 라이브러리 statistics 모듈의 median 은 숫자 묶음의 중앙값을 돌려줍니다. 짝수 건에서는 가운데 둘의 평균을 씁니다.

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

첫 줄은 세 건이라 가운데 한 건인 3 이 나옵니다. 둘째 줄은 네 건이라 가운데 둘인 3 과 5 의 평균 4.0 이 나옵니다. 4.0 은 원래 묶음에 없는 값입니다.

median 이 내는 수는 극단값에 덜 휘둘리는 강건한 척도입니다. 강건하다는 것은 값 몇 개가 크게 틀어져도 결과가 크게 안 바뀐다는 뜻입니다. 빈 묶음을 넘기면 StatisticsError 를 냅니다.

PostgreSQL percentile_cont

PostgreSQL 은 SQL(Structured Query Language) 질의 안에서 중앙값을 계산합니다. 분위수 함수 percentile_cont 에 비율 0.5 를 넘깁니다.

SQL
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY income) FROM households;
 percentile_cont
-----------------
           50489

households 테이블의 income 열에서 50번째 백분위수, 곧 중앙값을 구한 결과가 50489 입니다. WITHIN GROUP (ORDER BY income) 이 값을 줄 세우는 부분입니다. 0.5 는 줄의 어느 비율에서 읽을지를 정합니다. 이 함수는 null 값을 빼고 계산합니다.

Four Keys 의 변경 리드 타임

Four Keys 는 DORA(DevOps Research and Assessment) 팀이 소프트웨어 배포 성과를 네 지표로 재려고 낸 오픈소스 프로젝트입니다. 지금은 유지보수가 멈춘 상태입니다. Four Keys 에서 변경 리드 타임은 「커밋 하나가 프로덕션에 배포되기까지 걸린 시간의 중앙값」입니다.

배포마다 그 배포에 든 커밋들의 시간 차를 분 단위로 잽니다. 이어서 하루 단위로 묶어 중앙값을 냅니다. 이 프로젝트가 쓰는 SQL 질의의 해당 부분입니다.

SQL
PERCENTILE_CONT(
  # Ignore automated changes
  IF(time_to_change_minutes > 0, time_to_change_minutes, NULL),
  0.5) # Median
  OVER (partition by day) AS median_time_to_change

partition by day 가 하루씩 묶습니다. 0.5 가 그 하루의 중앙값을 고릅니다. 걸린 시간이 0 이하인 건은 NULL 로 바꿔 계산에서 뺍니다. 자동화된 변경을 계산에 안 넣는 처리입니다.

flowchart TD
    subgraph D["어느 하루"]
        P1["배포 1"] --> C1["커밋 a 의 시간 차"]
        P1 --> C2["커밋 b 의 시간 차"]
        P2["배포 2"] --> C3["커밋 c 의 시간 차"]
    end
    C1 --> Z["0 이하인 시간 차는 뺀다"]
    C2 --> Z
    C3 --> Z
    Z --> M["그날 시간 차 전체의 중앙값"]

줄을 세우는 단위는 배포가 아니라 커밋 하나하나입니다. 그날 모든 배포에 든 커밋의 시간 차를 한 줄로 세워 한가운데를 읽습니다.

서비스 복구 시간도 중앙값으로 정의합니다. 장애를 낸 배포부터 연결된 버그·장애 보고가 닫히기까지 걸린 시간의 중앙값입니다.

DORA 의 현행 지표는 다섯 개로 늘었습니다. 현행 정의의 리드 타임은 「버전 관리에 커밋된 뒤 프로덕션에 배포되기까지 걸린 시간」입니다. 이 정의에는 중앙값이라는 말이 없습니다.

대가

중앙값은 「절반의 요청이 이 시간 안에 끝난다」에 답하는 수입니다. 응답 시간 묶음을 이 수 하나로 줄이면 나머지 값들이 어디에 있었는지는 남지 않습니다. 계산하는 쪽도 평균보다 많은 것을 들고 있어야 합니다.

꼬리를 못 본다

중앙값은 뒤쪽 절반이 어떤 모양이든 같은 수를 냅니다. 뒤쪽 절반이 전부 0.2초여도, 그중 한 건이 30초여도 중앙값은 같습니다. 가장 오래 기다린 사용자는 이 수에 하나도 안 드러납니다.

그래서 성능 지표는 p50 하나로 끝내지 않습니다. 뒤쪽 끝을 보는 p95 같은 높은 분위수를 같이 적습니다. 꼬리 지연은 그쪽이 받습니다.

원래 값을 전부 들고 있어야 한다

평균은 합과 건수 두 수만 들고 있으면 됩니다. 새 값이 오면 합에 더하고 건수를 하나 올립니다. 중앙값은 이렇게 갱신할 수 없습니다. 한가운데가 어디인지 알려면 모든 값의 순위를 알아야 합니다.

가장 단순한 방법은 정렬입니다. 건수가 n 이면 비용이 n log n 에 비례합니다. 한가운데 한 건만 찾는 선택 알고리즘을 쓰면 평균적으로 n 에 비례하는 비용으로 줄어듭니다. 어느 쪽이든 값은 전부 들고 있어야 합니다.

값이 끝없이 흘러드는 모니터링에서는 이 비용을 두 방식 중 하나로 치릅니다. 모니터링 도구인 Prometheus 가 둘을 다 둡니다.

방식 하는 일 치르는 값
서머리 재는 프로그램 안에서 분위수를 미리 계산해 내보낸다 그 프로그램이 자원을 쓴다. 다른 시간 창·다른 분위수로 나중에 다시 계산할 수 없다
히스토그램 값이 어느 구간에 몇 건 들어왔는지만 센다. 분위수는 나중에 서버가 계산한다 원래 값이 없어 구간 안의 값을 근사한다

서머리의 분위수는 쓰는 알고리즘에 따라 대개 매우 정확합니다. 다만 서머리로 내든 서버에서 내든 분위수는 근삿값입니다. 근사가 어떻게 갈리는지는 「경계」가 다룹니다.

묶음끼리 합칠 수 없는 까닭

평균은 묶음끼리 다시 합칠 수 있습니다. 묶음마다 평균에 건수를 곱해 더합니다. 그 합을 전체 건수로 나누면 됩니다. 중앙값에는 그런 방법이 없습니다.

서버 A 가 잰 세 건은 1, 2, 30 입니다. 서버 B 가 잰 세 건은 4, 5, 6 입니다. 각각의 중앙값은 2 와 5 입니다. 두 수를 다시 중앙값으로 묶으면 3.5 가 나옵니다. 여섯 건을 한 줄로 다시 세우면 1, 2, 4, 5, 6, 30 입니다. 한가운데 두 건의 평균은 4.5 입니다.

flowchart TD
    subgraph SA["서버 A"]
        A["1 · 2 · 30"]
    end
    subgraph SB["서버 B"]
        B["4 · 5 · 6"]
    end
    A --> M1["A 의 중앙값 · 2"]
    B --> M2["B 의 중앙값 · 5"]
    M1 --> R1["두 중앙값을 다시 중앙값으로 · 3.5"]
    M2 --> R1
    A --> J["여섯 건을 한 줄로 · 1 · 2 · 4 · 5 · 6 · 30"]
    B --> J
    J --> R2["한가운데 두 건의 평균 · 4.5"]

같은 여섯 건에서 결과가 둘로 갈립니다. 묶음마다 중앙값 하나만 남기면 나머지 네 건이 어디에 있었는지가 사라지기 때문입니다.

모니터링에서 서버를 여러 대 띄우면 이 문제를 바로 만납니다. Prometheus 의 서머리가 서버마다 낸 분위수는 중앙값이든 90번째 백분위수든 합칠 수 없습니다. 미리 계산해 둔 분위수를 합치는 것은 대개 뜻이 없습니다.

promql
avg(http_request_duration_seconds{quantile="0.95"}) // BAD!
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) // GOOD.

첫 줄은 서버마다 낸 95번째 백분위수의 평균입니다. 이 사례에서 분위수를 평균 내면 통계적으로 뜻이 없는 수가 나옵니다. 둘째 줄은 히스토그램의 구간별 건수를 먼저 모든 서버에 걸쳐 더합니다. 그리고 합친 구간에서 분위수를 계산합니다.

구간 경계가 서버마다 같으면 구간 건수는 더할 수 있으므로 서버끼리 합칠 수 있습니다. 위 질의는 95번째 백분위수를 계산합니다. 중앙값은 비율이 0.5 인 분위수입니다.

건수를 곱해 합계를 낼 수 없다

평균에 건수를 곱하면 합계가 나옵니다. 요청 한 건의 평균 전송량에 그날 요청 수를 곱하면 그날 전송량 전체입니다.

중앙값에는 이 성질이 없습니다. 큰 값이 몇 건 섞여 있으면 합계를 크게 밑도는 수가 나옵니다. 합계나 용량을 잡는 계산에는 평균을 씁니다.

경계

도구가 p50 이라고 찍은 수는 이 문서가 정의한 중앙값과 같은 수인가. 원래 값을 전부 들고 짝수 건에서 가운데 둘의 평균을 내는 도구라면 같은 수입니다. 그 밖의 도구가 낸 p50 은 중앙값을 다른 방식으로 계산한 값이거나 근삿값입니다. 같은 묶음에서 다른 수가 나올 수 있습니다.

갈리는 곳은 두 군데입니다. 하나는 짝수 건에서 가운데 둘을 다루는 방식입니다. 다른 하나는 원래 값 없이 구간 건수만으로 계산하는지입니다.

flowchart TD
    Q["도구가 찍은 p50"] --> K{"원래 값을 전부 들고 계산하나"}
    K -->|"아니다 · 구간별 건수만 있다"| E["구간과 가정에서 얻은 근삿값"]
    K -->|"그렇다"| O{"짝수 건에서 가운데 둘을"}
    O -->|"평균 낸다"| M["이 문서의 중앙값"]
    O -->|"한쪽을 고른다"| L["원래 값 가운데 하나"]

짝수 건 처리는 같은 Python statistics 안에서도 셋으로 갈립니다. 네 건 [1, 3, 5, 7] 에서 나오는 수가 다릅니다.

함수 짝수 건에서 하는 일 [1, 3, 5, 7] 의 결과
median 가운데 둘의 평균 4.0
median_low 가운데 둘 중 작은 쪽 3
median_high 가운데 둘 중 큰 쪽 5

median_low 와 median_high 는 결과가 언제나 원래 묶음의 값입니다. 값끼리 크기 순서는 매길 수 있지만 더하기가 안 되는 자료가 있습니다. 옷 치수 「스몰 · 미디엄 · 라지 · 엑스라지」가 그렇습니다. 가운데 둘이 미디엄과 라지면 그 둘의 평균이라는 치수가 없으니 이럴 때 이 둘을 쓰는 쪽을 고려합니다.

PostgreSQL 에서도 둘로 갈립니다. percentile_cont 는 필요하면 이웃한 두 값 사이를 보간합니다. 보간은 두 값 사이의 값을 비율로 채워 넣는 계산입니다.

percentile_disc 는 순서상 위치가 비율 이상인 첫 값을 돌려줍니다. 줄에서 그 비율에 처음 닿는 값을 그대로 쓰므로 결과는 원래 값 가운데 하나입니다.

구간 건수만 있는 도구는 원래 값이 없습니다. Prometheus 의 histogram_quantile 은 분위수가 든 구간을 찾아 그 구간 안에서 위치를 잡습니다. 구간을 어떻게 잡아 두었는지에 따라 이 함수가 보는 히스토그램이 두 갈래입니다. 클래식 히스토그램과 네이티브 히스토그램입니다.

클래식 히스토그램에서는 구간 안에 값이 고르게 퍼져 있다고 보고 위치를 보간합니다. 이것을 선형 보간이라고 부릅니다.

네이티브 히스토그램에는 구간 폭을 뒤로 갈수록 키우는 표준 규격이 따로 있습니다. 이 규격으로 잡은 구간에서는 가정이 대개 달라집니다. 그 구간을 더 잘게 쪼개면 나올 칸들에 값이 고르게 들어찬다고 봅니다. 이것을 지수 보간이라고 부릅니다.

맨 위 구간은 예외입니다. 분위수가 거기 떨어지면 보간하지 않고 그 아래 구간의 위쪽 경계를 돌려줍니다. 이 예외를 둔 까닭은 확인되지 않았습니다(미확인). 어느 쪽이든 결과는 원래 값의 중앙값이 아니라 구간과 가정에서 얻은 근삿값입니다.

구간으로 묶은 자료에는 가정이 하나 더 붙습니다. Python median_grouped 의 예가 그 가정을 보여 줍니다. 이 함수는 값이 일정한 폭의 구간들로 묶여 구간의 가운데 값으로 적힌 자료를 받습니다. 1071명의 나이를 10살 구간으로 묶은 자료에서 한가운데인 536번째 사람은 30~40살 구간에 듭니다.

이 자료에는 원래 나이가 없습니다. 30대인 484명이 모두 구간의 가운데 값인 35살로 적혀 있습니다. 그래서 median 은 35 를 냅니다. 이 수는 484명이 모두 정확히 35살이었다고 보는 가정의 결과입니다.

median_grouped(data, interval=10) 은 484명이 30에서 40 사이에 고르게 퍼졌다고 보고 37.5 를 냅니다. 484명이 30에서 40 사이에 퍼져 있다고 보는 쪽이 더 받아들이기 쉬운 가정입니다. 두 수의 차이는 어느 가정을 고르느냐에서 옵니다. 어느 쪽도 원래 나이의 중앙값은 아닙니다.

분위수를 계산하는 정의는 통계 패키지마다 여럿입니다. 한 패키지 안에서도 쓰는 곳에 따라 다른 정의를 쓰는 일이 흔합니다. 위 Python statistics 의 median · median_low · median_high · median_grouped 가 그 예입니다. 그래서 두 도구의 p50 을 견줄 때는 이름보다 계산 방식을 먼저 맞춰 봅니다.

관련 항목

중앙값과 나란히 분포를 요약하는 통계량

평균 · 최빈값 · 대표값 · 분산 · 표준편차 · 범위 · 사분위 범위 · 요약 통계 · 분포

중앙값을 눈금 하나로 품는 분위수 이름

분위수 · 백분위수 · 사분위수 · 십분위수 · 높은 분위수 · p95 · 누적 분포 함수

중앙값을 계산하거나 근사하는 방법

정렬 · 선택 알고리즘 · 퀵셀렉트 · 중앙값의 중앙값 · 히스토그램 · 보간 · 선형 보간 · t-digest · 근사 분위수

중앙값을 계산해 주는 함수와 도구

Python · PostgreSQL · percentile_cont · percentile_disc · Prometheus · histogram_quantile · 서머리 · BigQuery

중앙값이 가려 버리는 치우친 값

이상치 · 꼬리 지연 · 긴 꼬리 · 치우침 · 지수 분포 · 코시 분포 · 조율된 누락

중앙값으로 요약하는 성능 지표

응답 시간 · 지연 · 처리량 · 서비스 수준 목표 · 평균 복구 시간

중앙값으로 집계하는 배포 지표

변경 리드 타임 · 서비스 복구 시간 · 배포 빈도 · 변경 실패율 · Four Keys · DORA

중앙값을 도구로 쓰는 분야

데이터 분석 · 성능 · 모니터링 · 용량 계획 · 탐색적 자료 분석 · 강건 통계

다른 이름: median · 중위값 · 중위수 · p50