Blog
pysparksparkquantilepercentilestatisticsdata-engineering

PySpark 대규모 분위수·통계 — p95 지연시간을 정확히, 빠르게

수십억 행에서 중앙값·백분위(p95/p99)를 구하는 일이 왜 어려운지, 정확한 분위수의 비용과 approxQuantile(근사)의 동작, 그룹별 분위수, 이상치 탐지(IQR), 상관·공분산까지 대규모 통계 계산 패턴을 정리합니다.

Data Dynamics2026年6月5日11 min read
This post is not yet translated. The original Korean version is shown below.

팀장이 "API 응답시간 p95 를 구해줘"라고 했을 때, 여러분은 잠깐 멈칫하셨나요? 평균이라면 어디서든 뚝딱 나오지만, p95 는 수십억 행에서 정직하게 구하려는 순간 클러스터가 신음합니다. 백분위(percentile)와 중앙값(median)은 본질적으로 전체를 정렬해야 정확히 구할 수 있고, 분산 환경에서 전역 정렬은 비싼 셔플이거든요.

이 글에서 배우는 것

  • 분위수가 평균보다 왜 근본적으로 비싼지
  • approxQuantile(근사 분위수)이 어떻게 정렬 없이 빠른 답을 내는지
  • 그룹별 p95, IQR 이상치 탐지, 상관·공분산까지 실전 패턴
  • 샘플링으로 통계 비용을 한 번 더 줄이는 방법

1. 왜 분위수는 평균보다 어려운가

먼저 두 통계의 본질적인 차이를 짚어 봅시다.

평균(avg)     : sum / count → 부분합을 더하면 됨 (가산적, 셔플 적음)
중앙값/p95    : 정렬된 위치의 값 → 전체 순서를 알아야 함 (전역 정렬 필요)

평균·합·최댓값은 결합법칙이 성립해 각 파티션에서 부분 결과를 구해 합치면 됩니다. 반면 "정확히 95번째 백분위"는 모든 값의 전체 순서를 알아야 하므로, 정확한 계산은 전역 정렬(또는 그에 준하는 셔플)을 요구하죠.

한 문장으로: 더하면 되는 통계(평균)는 싸고, 줄 세워야 하는 통계(분위수)는 비쌉니다.

통계비용
count, sum, avg, min, max싸다(가산적)
stddev, var싸다(가산적)
median, percentile비싸다(정렬)
distinct count비싸다(별도 글 "고카디널리티 집계")

2. 정확한 분위수 — percentile

계약·정산처럼 정확한 값이 반드시 필요한 상황이라면 percentile 을 씁니다. 단, 비용이 크다는 점을 알고 쓰세요.

from pyspark.sql import functions as F
 
# 정확한 분위수 (정렬 비용)
df.agg(
    F.percentile("latency", 0.5).alias("median"),
    F.percentile("latency", 0.95).alias("p95"),
    F.percentile("latency", 0.99).alias("p99"))
 
# percentile_approx 의 정확 버전 또는 expr
df.selectExpr("percentile(latency, array(0.5, 0.95, 0.99)) as pcts")

percentile 은 모든 값을 고려해 정확하지만, 대규모에서 메모리·시간이 큽니다.

3. 근사 분위수 — approxQuantile (권장)

사실 대부분의 모니터링·분석 지표는 약간의 오차가 허용됩니다. p95 가 230ms 이든 232ms 이든 "느리다"는 판단은 같거든요. approxQuantile고정 메모리의 스케치로 분위수를 근사해, 정렬 없이 빠르게 계산합니다. 실무에서는 이 함수를 기본값으로 두는 것이 정석입니다.

# DataFrame.approxQuantile(col, probabilities, relativeError)
quantiles = df.approxQuantile(
    "latency",
    [0.5, 0.95, 0.99],
    0.001)                  # 상대 오차 (작을수록 정확·비용↑)
# → [median, p95, p99]
 
# SQL 표현식 버전 (그룹·집계에 사용)
df.groupBy("endpoint").agg(
    F.percentile_approx("latency", 0.95, 10000).alias("p95"))
percentile(정확)approxQuantile(근사)
정확도100%relativeError 내
메모리고정(작음)
속도느림빠름
적합정산·규정모니터링·분석

relativeError 트레이드오프: 0.0 이면 정확하지만 비싸고, 0.01(1%)이면 매우 빠릅니다. SLA 모니터링은 보통 0.001~0.01 로 충분합니다.

팁: 분위수도 distinct 처럼 "정확함이 정말 필요한가"를 먼저 자문해 보세요. p95 지연 같은 지표는 근사로 충분하고, 잡이 수배 빨라집니다.

4. 그룹별 분위수

엔드포인트별·국가별 p95 처럼 그룹을 쪼개서 분위수를 봐야 하는 경우도 많죠. percentile_approx 를 집계 함수로 그대로 쓰면 됩니다.

result = (df.groupBy("endpoint")
    .agg(
        F.percentile_approx("latency", 0.5, 10000).alias("p50"),
        F.percentile_approx("latency", 0.95, 10000).alias("p95"),
        F.percentile_approx("latency", 0.99, 10000).alias("p99"),
        F.avg("latency").alias("avg")))

세 번째 인자(accuracy)가 클수록 정확하고 메모리를 더 씁니다. 그룹이 많고 스큐하면 비용이 커지니(별도 글 "데이터 스큐"), 스큐 그룹을 점검하세요.

5. 이상치 탐지 — IQR

분위수의 대표적인 응용이 바로 이상치 탐지입니다. 상자 수염 그림을 떠올리면 이해하기 쉽죠. IQR(사분위 범위) 로 정상 범위를 벗어난 값을 찾아낼 수 있습니다.

# Q1, Q3 근사 계산
q1, q3 = df.approxQuantile("amount", [0.25, 0.75], 0.001)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
 
# 이상치 플래그
flagged = df.withColumn("is_outlier",
    (F.col("amount") < lower) | (F.col("amount") > upper))
 
outliers = flagged.filter("is_outlier")
방법기준
IQRQ1-1.5·IQR ~ Q3+1.5·IQR 벗어남
Z-score평균에서 N 표준편차 밖
백분위 컷상하위 1% 제거

Z-score 는 가산적 통계(평균·표준편차)만 쓰므로 더 싸지만, 데이터 분포가 정규가 아니라면 IQR 이 훨씬 견고합니다.

6. 상관·공분산, 요약 통계

데이터를 처음 탐색할 때, 여러 변수 간 관계나 전반적 분포를 빠르게 훑어보고 싶을 때가 있습니다. PySpark 는 이를 위한 편리한 도구들을 기본 제공합니다.

# 상관계수 / 공분산
df.stat.corr("price", "demand")
df.stat.cov("price", "demand")
 
# 요약 통계 (count, mean, stddev, min, max, 분위수)
df.summary("count", "mean", "stddev", "min", "25%", "50%", "75%", "max").show()
 
# 빈도 교차표 / 근사 빈발항목
df.stat.crosstab("device", "status")
df.stat.freqItems(["product_id"], support=0.01)

summary() 는 EDA(탐색적 분석)에 유용하지만 분위수를 포함하므로 큰 데이터에서는 비쌀 수 있습니다 — 샘플에 적용하는 것도 방법입니다.

7. 샘플링으로 통계 비용 줄이기

모든 분석이 전체 데이터를 볼 필요는 없습니다. 분포를 추정하거나 EDA 를 할 때는 대표 표본으로도 충분한 경우가 많거든요.

# 단순 무작위 표본
sample = df.sample(fraction=0.01, seed=42)
 
# 층화 표본 (그룹 비율 유지)
stratified = df.sampleBy("country",
    fractions={"KR": 0.1, "US": 0.05, "JP": 0.1}, seed=42)
 
# 표본으로 분위수 추정
sample.approxQuantile("latency", [0.95], 0.001)

분포 추정·EDA 단계에서는 1% 표본으로도 충분한 경우가 많아, 비용을 크게 줄입니다.

직관: 근사 분위수 + 샘플링을 함께 쓰면 "근사의 근사"가 아니라, 대부분의 의사결정에 충분한 빠른 답이 됩니다.

8. 정리

통계도구비용
평균·합·표준편차avg, sum, stddev싸다
정확 분위수percentile비싸다
근사 분위수approxQuantile/percentile_approx싸다(권장)
그룹별 p95percentile_approx in agg중간
이상치IQR(approxQuantile) / Z-score중간/싸다
관계stat.corr, summary중간

대규모 통계 계산의 핵심은 "가산적 통계는 싸고, 순서가 필요한 통계(분위수)는 비싸다"는 구분입니다. p95·중앙값이 필요하면 정확한 percentile 대신 approxQuantile 을 기본으로 삼으세요 — 고정 메모리로 정렬 없이, 허용 오차 안에서 빠르게 답을 줍니다. 여기에 IQR 이상치 탐지와 샘플링을 더하면, 수십억 행의 통계 분석도 일정한 비용으로 처리할 수 있습니다.

마치며 — 핵심 요약

  • 평균은 더하면 되지만 분위수는 줄을 세워야 한다 — 이 차이가 비용의 근본 원인입니다.
  • approxQuantile / percentile_approx 를 기본값으로 사용하세요. 모니터링·분석에서 근사 오차는 거의 문제가 되지 않습니다.
  • relativeError0.001~0.01 이 실무에서 속도와 정확도의 좋은 균형점입니다.
  • 이상치 탐지에는 IQR 을 — 분포가 치우쳐 있어도 견고하게 동작합니다.
  • EDA 단계라면 1% 샘플링 먼저 — 전체 데이터를 돌리기 전에 표본으로 검증하는 습관이 비용을 아낍니다.
  • 정산·규정처럼 정확도가 필수인 경우에만 percentile 을 꺼내 쓰면 됩니다.

데이터가 수십억 행이어도 올바른 도구를 고르면 분석 시간을 수분에서 수십 초로 줄일 수 있습니다. 여러분의 다음 p95 쿼리가 훨씬 가벼워지기를 바랍니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. 대규모 지표·통계 분석이나 이상치 탐지 파이프라인 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀