본문으로 건너뛰기
김신건의 로그

[Pandas] Categorical

· 수정 · 📖 약 1분 · 436자/단어 #python #pandas #categorical #dtype #memory
Pandas Categorical, pd.Categorical, category dtype, 카테고리 dtype, pandas 카테고리

정의

Categorical제한된 고유값 만 가질 수 있는 dtype. 내부적으로 정수 코드로 저장되어 메모리 절약속도 향상 을 동시에 달성한다.

고유값 수가 적고(low-cardinality) 반복이 많은 열에 적합하다. 예: 도시, 등급, 상태 코드.

내부 구조

object dtype 은 각 셀이 Python str 객체를 직접 가리키는 포인터 배열이다. category dtype 은 고유값 사전(categories) 과 정수 코드 배열(codes) 두 가지로 분리한다.

flowchart TD
    subgraph "object dtype - 100만 행"
        O1["ptr -> 'Seoul'"]
        O2["ptr -> 'Busan'"]
        O3["ptr -> 'Seoul'"]
        O4["... x 1000000"]
    end

    subgraph "category dtype"
        C1["codes: 0, 1, 0, ... (int8 x 100만)"]
        C2["categories: 'Seoul', 'Busan', 'Daegu'"]
    end

    O4 -.->|"astype category"| C1
  • object: 포인터 1개 = 8 bytes, str 객체 자체 = 50+ bytes, 총 100만 개
  • category: int8 코드 1 byte x 100만 + 문자열 3개만 저장

기본 사용

df['city'] = df['city'].astype('category')

# 정렬 가능한 카테고리
df['grade'] = pd.Categorical(df['grade'], categories=['F','D','C','B','A'], ordered=True)
python
import pandas as pd
import numpy as np
np.random.seed(0)
cities = np.random.choice(['Seoul', 'Busan', 'Daegu'], size=100_000)
df = pd.DataFrame({'city': cities})

mem_object = df.memory_usage(deep=True).sum()
df['city'] = df['city'].astype('category')
mem_cat = df.memory_usage(deep=True).sum()
print(f'object  : {mem_object:>10,} bytes')
print(f'category: {mem_cat:>10,} bytes')
print(f'절감    : {(1 - mem_cat/mem_object)*100:.1f}%')
결과
object  :  6,300,128 bytes
category:    100,468 bytes
절감    : 98.4%

언제 유리한가

상황이유
고유값이 적고 반복이 많음코드 배열로 압축 (98%+ 절감 가능)
groupby / merge / sort정수 비교가 문자열 비교보다 빠름
명시적 순서가 필요한 등급ordered=True 로 비교 연산 가능
Parquet / Feather 저장dtype 그대로 직렬화

언제 비효율적인가

  • 거의 모든 값이 unique (ID, free-form text) → 코드 압축 효과 없음
  • 자주 새 카테고리가 추가됨 → add_categories 오버헤드
  • 산술 연산이 필요한 수치형 데이터 → category 는 산술 불가

ordered categorical

grades = pd.Categorical(
    df['grade'],
    categories=['F', 'D', 'C', 'B', 'A'],
    ordered=True,
)
df['grade'] = grades

# 비교 연산 가능 (사전순이 아니라 정의 순서 기준)
df[df['grade'] > 'C']        # B, A 행만

# min, max 도 순서 기반
df['grade'].min()   # 'F'
df['grade'].max()   # 'A'

IMPORTANT

ordered=True 없이 >, < 비교를 하면 TypeError 가 발생한다. 등급, 우선순위처럼 순서가 의미 있는 데이터에만 ordered=True 사용.

CategoricalDtype

재사용 가능한 dtype 객체를 정의해 여러 컬럼이나 DataFrame 에 적용할 수 있다.

from pandas import CategoricalDtype

size_type = CategoricalDtype(
    categories=['XS', 'S', 'M', 'L', 'XL'],
    ordered=True,
)

df['size'] = df['size'].astype(size_type)

# read_csv 에서 바로 적용
df = pd.read_csv('data.csv', dtype={'size': size_type})

.cat accessor

s = pd.Series(['low', 'high', 'med'], dtype='category')

s.cat.categories                          # Index(['high', 'low', 'med'])
s.cat.codes                               # 정수 코드 [1, 0, 2]
s.cat.ordered                             # False

s.cat.rename_categories({'low': 'L', 'high': 'H', 'med': 'M'})
s.cat.add_categories(['extra'])           # 새 카테고리 추가
s.cat.remove_categories(['low'])          # 카테고리 제거 (해당 값은 NaN)
s.cat.remove_unused_categories()          # 실제로 등장하지 않는 카테고리 정리
s.cat.set_categories(['L', 'M', 'H'], ordered=True)
s.cat.reorder_categories(['H', 'M', 'L'], ordered=True)

pd.cut / pd.qcut 과의 연계

pd.cutpd.qcut 은 기본적으로 category dtype 을 반환한다.

df['age_group'] = pd.cut(
    df['age'],
    bins=[0, 20, 40, 60, 100],
    labels=['10대', '30대', '50대', '70대+'],
    ordered=True,
)
# dtype: category (ordered)
# 직접 groupby 가능
df.groupby('age_group')['revenue'].sum()
# qcut: 분위수 기반 분할
df['quartile'] = pd.qcut(df['score'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

groupby 성능

import time

df_large = pd.DataFrame({'city': np.random.choice(['Seoul','Busan','Daegu'], 1_000_000)})
df_large['amount'] = np.random.randn(1_000_000)

# object dtype
t = time.perf_counter()
df_large.groupby('city')['amount'].sum()
print(f'object  : {time.perf_counter()-t:.4f}s')

df_large['city_cat'] = df_large['city'].astype('category')
t = time.perf_counter()
df_large.groupby('city_cat')['amount'].sum()
print(f'category: {time.perf_counter()-t:.4f}s')
  • object: 매번 문자열 해싱 + 비교
  • category: 정수 코드 기반 그룹화, 훨씬 빠름

downcasting 전략

object 열을 일괄 category 로 변환하는 패턴.

def downcast_object_columns(df: pd.DataFrame, threshold: float = 0.5) -> pd.DataFrame:
    """고유값 비율이 threshold 미만인 object 컬럼을 category 로 변환."""
    for col in df.select_dtypes('object').columns:
        ratio = df[col].nunique() / len(df)
        if ratio < threshold:
            df[col] = df[col].astype('category')
    return df

df = downcast_object_columns(df)

함정

1. category 에서 산술 연산 불가

s = pd.Series([1, 2, 3], dtype='category')
s + 10        # ❌ TypeError
s.astype(int) + 10    # ✓

수치형 데이터는 category 로 변환하지 않는다. category저-cardinality 라벨 전용.

2. 새 값 할당 시 에러

df['city'] = df['city'].astype('category')
df.loc[0, 'city'] = 'Incheon'    # ❌ categories 에 없는 값

# 해법: add_categories 먼저
df['city'] = df['city'].cat.add_categories('Incheon')
df.loc[0, 'city'] = 'Incheon'    # ✓

3. CSV 저장 시 dtype 소실

df.to_csv('out.csv')
# read_csv 로 다시 읽으면 object 로 돌아옴

# 해법 A: 명시적 dtype 지정
pd.read_csv('out.csv', dtype={'city': 'category'})

# 해법 B: Parquet 사용 (dtype 보존)
df.to_parquet('out.parquet')
pd.read_parquet('out.parquet')   # category dtype 그대로

4. merge 시 카테고리 불일치

df1['grade'] = pd.Categorical(['A','B'], categories=['A','B','C'])
df2['grade'] = pd.Categorical(['B','C'], categories=['B','C','D'])

pd.merge(df1, df2, on='grade')
# categories 가 달라도 merge 는 되지만 결과 dtype 이 달라질 수 있음
# 정확한 동작을 원하면 merge 전 astype('str') 후 re-cast

WARNING

두 DataFrame 의 같은 컬럼이 서로 다른 categories 집합을 가지면, mergeconcat 후 예상치 못한 NaN 이 생길 수 있다. 합치기 전에 union_categoricals 또는 astype(str) 로 정규화하라.

관련 위키

이 글의 용어 (6개)
[Pandas] 성능 / 메모리 최적화pandas
정의 pandas 의 흔한 성능 함정과 최적화 패턴. 벡터화 + dtype 선택 + 알고리즘 의 조합이 핵심. 사용 상황 | 상황 | 권장 접근 | |:---|:---| | 단순…
[Pandas] cut / qcutpandas
정의 - : 값 기반 binning (구간을 직접 지정) - : 빈도 기반 binning (분위수로 같은 크기) 연속형 변수를 범주형으로 변환. ML 의 feature engin…
[Pandas] DataFramepandas
정의 은 2차원 레이블 테이블. 각 열이 , 모든 열이 같은 (행 라벨) 를 공유. SQL 테이블 / Excel 시트 / R data.frame 의 Python 대응체. 구조 시…
[Pandas] groupbypandas
정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
[Pandas] Nullable Types (Int64, boolean, string[pyarrow])pandas
정의 Nullable dtypes 는 NaN/None 을 타입 그대로 표현할 수 있는 pandas 의 새 타입 시스템. NumPy 기반의 (NaN 미허용) 의 한계를 극복. - …
[Pandas] replace / astypepandas
정의 - : 특정 값을 다른 값으로 치환 - : dtype 변환 dtype 변환 경로 replace 기본 <CodeWithOutput language="python" output…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기