본문 바로가기
자격증/빅데이터 분석 기사

pandas groupby에 없는 조합이 생길 때: observed와 범주형 집계

by char_lie 2026. 10. 8.
반응형

집계 결과에 원본에 없던 지역·채널 조합이 생기면 그룹 키가 범주형인지 확인하고 observed를 명시하세요. observed=True는 관측된 범주형 조합만 남기고, False는 사용하지 않은 범주 조합까지 집계 결과에 포함할 수 있습니다.

Python 3.12.14·pandas 2.2.3에서 실행한 파일 없는 예제입니다. pandas 3.0부터는 observed의 기본값이 True로 바뀌었으므로, 다른 버전에서 같은 보고서를 재현하려면 기본값에 기대지 않는 편이 좋습니다.

범주형 분류 선반에서 실제 데이터가 있는 칸과 비어 있는 칸을 구분한 그림
범주 목록과 실제 관측 조합은 서로 다를 수 있습니다.

1. 값 세 개와 범주 목록을 구분합니다

import pandas as pd

df = pd.DataFrame({
    "region": pd.Categorical(
        ["Seoul", "Seoul", "Busan"],
        categories=["Seoul", "Busan", "Jeju"]
    ),
    "channel": pd.Categorical(
        ["web", "app", "web"],
        categories=["web", "app"]
    ),
    "sales": [10, 20, 30]
})
print(df.to_string(index=False))
print(df["region"].dtype, df["channel"].dtype)
region channel  sales
 Seoul     web     10
 Seoul     app     20
 Busan     web     30
category category

관측 행은 세 개지만 region에는 Jeju도 등록되어 있습니다. channel의 범주 두 개와 조합하면 분류 틀은 3×2=6칸입니다. categories에 이름이 있다는 사실이 그 조합의 데이터가 실제로 있다는 뜻은 아닙니다.

2. observed만 바꿔 집계 행 수를 비교합니다

all_groups = df.groupby(
    ["region", "channel"], observed=False, sort=True
).size()
seen_groups = df.groupby(
    ["region", "channel"], observed=True, sort=True
).size()

print("전체 범주 조합:", len(all_groups))
print(all_groups.reset_index(name="rows").to_string(index=False))
print("관측 조합:", len(seen_groups))
print(seen_groups.reset_index(name="rows").to_string(index=False))
전체 범주 조합: 6
region channel  rows
 Seoul     web     1
 Seoul     app     1
 Busan     web     1
 Busan     app     0
  Jeju     web     0
  Jeju     app     0
관측 조합: 3
region channel  rows
 Seoul     web     1
 Seoul     app     1
 Busan     web     1

False 결과의 Busan·app, Jeju·web, Jeju·app은 원본에 없습니다. size가 0인 빈 조합입니다. True 결과에는 Seoul·web, Seoul·app, Busan·web의 세 조합만 나타납니다. observed는 원본 행을 만들어 내는 옵션이 아니라 집계 결과의 범주 표시 범위를 정하는 옵션입니다.

전체 분류 틀과 실제 데이터가 담긴 묶음을 비교하는 그림
observed=True는 실제 관측된 범주형 조합을 집계합니다.

3. 빈 조합의 합계 0을 관측값 0과 섞지 않습니다

summary = df.groupby(
    ["region", "channel"], observed=False, sort=True
).agg(
    rows=("sales", "size"),
    total=("sales", "sum"),
    average=("sales", "mean")
)
print(summary.reset_index().to_string(index=False))
region channel  rows  total  average
 Seoul     web     1     10     10.0
 Seoul     app     1     20     20.0
 Busan     web     1     30     30.0
 Busan     app     0      0      NaN
  Jeju     web     0      0      NaN
  Jeju     app     0      0      NaN

이 예제에서 빈 조합은 rows=0, total=0, average=NaN입니다. 합계의 0만 보고 “그 지역의 매출이 실제 0이었다”라고 결론 내리면 안 됩니다. 행 수를 함께 확인하면 “관측 없음”과 “관측했지만 값이 0”을 구분하는 데 도움이 됩니다.

다른 집계 함수나 결측값 정책은 빈 그룹의 결과가 다를 수 있습니다. 위 출력은 size·sum·mean을 사용한 이 예제의 결과이며, NaN을 일괄 0으로 채우기 전에 보고서의 의미를 정해야 합니다.

4. 일반 문자열 열에는 같은 차이가 없습니다

plain = df.copy()
plain["region"] = plain["region"].astype("object")
plain["channel"] = plain["channel"].astype("object")

print(len(plain.groupby(
    ["region", "channel"], observed=False
).size()))
print(len(plain.groupby(
    ["region", "channel"], observed=True
).size()))
3
3

observed는 범주형 그룹 키에 적용됩니다. object 문자열로 바꾼 예제에는 별도의 미사용 범주 목록이 없으므로 두 설정 모두 관측된 세 조합을 반환합니다. df.dtypes와 Series.cat.categories를 먼저 확인하세요.

분류 설계도와 집계 결과를 함께 확인하는 검토 그림
빈 조합이 필요한 보고서인지 먼저 정하고 observed를 명시하세요.

모든 조합을 정해진 순서로 표시해야 하는 보고서에는 False가 적절할 수 있습니다. 실제 거래가 있었던 조합만 분석할 때는 True가 적절합니다. 여러 비범주형 키와 혼합하거나 범주가 많은 자료에서는 필요한 조합 수와 메모리도 먼저 점검하세요.

5. observed·dropna·size를 각각 결정합니다

observed는 미사용 범주 조합, dropna는 결측 그룹 키, size·count는 집계 대상의 차이입니다. 빈 조합 문제를 dropna로 해결하려고 하지 마세요. 결측 키 때문에 행이 사라지는 상황은 groupby의 dropna=False와 size·count 구분에서 이어서 확인할 수 있습니다.

정리: 범주형 여부 → 범주 목록 → observed 명시 → 결과 행 수 → 빈 그룹의 집계값 순서로 확인합니다. 실습에서는 원본을 보존하고 옵션 하나만 바꿔 결과를 비교하면 원인을 빠르게 좁힐 수 있습니다.

반응형

댓글