Blog
pysparksparkquantilepercentilestatisticsdata-engineering

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

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

Data Dynamics2026년 6월 5일8 min read

"API 응답시간 p95 를 구해줘." 간단해 보이는 이 요청이 수십억 행에서는 까다롭습니다. 백분위(percentile)와 중앙값(median)은 본질적으로 전체를 정렬해야 정확히 구할 수 있고, 분산 환경에서 전역 정렬은 비싼 셔플입니다. 평균은 쉽지만 분위수는 어렵습니다.

이 글은 대규모 분위수가 왜 비싼지, 근사 분위수(approxQuantile)가 어떻게 이를 해결하는지, 그리고 그룹별 분위수·이상치 탐지·상관분석 같은 통계 계산 패턴을 정리합니다.

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. 상관·공분산, 요약 통계

여러 변수 간 관계나 전반적 분포를 빠르게 봅니다.

# 상관계수 / 공분산
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. 샘플링으로 통계 비용 줄이기

전체가 아닌 대표 표본으로도 충분한 통계가 많습니다.

# 단순 무작위 표본
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 이상치 탐지와 샘플링을 더하면, 수십억 행의 통계 분석도 일정한 비용으로 처리할 수 있습니다.


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

— Data Dynamics 엔지니어링 팀