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

[Pandas] cut / qcut

· 수정 · 📖 약 1분 · 485자/단어 #python #pandas #binning #cut #qcut #categorization
Pandas cut / qcut, Pandas cut, Pandas qcut, binning pandas, pandas 구간화

정의

  • pd.cut(s, bins) : 값 기반 binning (구간을 직접 지정)
  • pd.qcut(s, q) : 빈도 기반 binning (분위수로 같은 크기)

연속형 변수를 범주형으로 변환. ML 의 feature engineering, 통계 보고서에 자주 사용.

등간격 vs 등빈도 binning

flowchart LR
    S["연속형 Series"] --> CUT["pd.cut()"]
    S --> QCUT["pd.qcut()"]

    CUT -->|"값 범위 기반"| C1["등간격 구간"]
    QCUT -->|"분위수 기반"| Q1["등빈도 구간"]

    C1 -->|"절대 기준에 적합"| R1["CategoricalDtype"]
    Q1 -->|"상대 기준에 적합"| R1

    R1 --> VC["value_counts()로 분포 확인"]
    R1 --> GB["groupby로 집계"]

cut, 값 기반

pd.cut(s, bins=[0, 18, 30, 50, 100])
pd.cut(s, bins=4)                      # 같은 간격 4 구간 자동
pd.cut(s, bins=[0,18,30,50,100], labels=['child','young','adult','senior'])
python
import pandas as pd
ages = pd.Series([5, 17, 25, 35, 55, 80])
print(pd.cut(ages, bins=[0, 18, 30, 50, 100],
  labels=['child','young','adult','senior']).tolist())
결과
['child', 'child', 'young', 'adult', 'senior', 'senior']
agebin
5child
17child
25young
35adult
55senior
80senior

qcut, 빈도 기반

pd.qcut(s, q=4)                       # 4분위 (각 25%)
pd.qcut(s, q=10, labels=False)        # 십분위, 정수 라벨
pd.qcut(s, q=[0, 0.5, 0.9, 1.0])      # 사용자 지정 quantile
python
import pandas as pd
scores = pd.Series([60, 65, 70, 75, 80, 85, 90, 95])
print(pd.qcut(scores, q=4, labels=['Q1','Q2','Q3','Q4']).tolist())
결과
['Q1', 'Q1', 'Q2', 'Q2', 'Q3', 'Q3', 'Q4', 'Q4']

각 그룹에 같은 수의 원소.

cut vs qcut 비교

항목cutqcut
분할 기준값 (예: 018, 1830)빈도 (각 25%)
그룹 크기다를 수 있음같음 (대략)
적합절대 기준 (나이, 가격대)상대 기준 (분위수, 등급)
NaN 처리NaN 그대로NaN 그대로
중복 경계해당 없음duplicates='drop' 필요

right / include_lowest

pd.cut(s, bins=[0, 18, 30], right=True)        # (left, right]
pd.cut(s, bins=[0, 18, 30], right=False)       # [left, right)
pd.cut(s, bins=[0, 18, 30], include_lowest=True)   # 가장 작은 구간이 [0, ...]

기본 right=True(0, 18]. 경계값이 어느 쪽에 들어가는지 명확히.

retbins (bin 경계도 반환)

bins_arr, edges = pd.qcut(s, q=4, retbins=True)
# 새 데이터에 같은 경계 적용 가능
pd.cut(new_s, bins=edges)

결과 활용

# 카테고리 dtype 반환
result = pd.cut(s, bins=[0, 18, 30, 50])
result.dtype                  # CategoricalDtype
result.value_counts()         # 각 구간의 빈도

# 정수 라벨이 필요하면
pd.cut(s, bins=4, labels=False)    # 0, 1, 2, 3

IntervalIndex

pd.cut / pd.qcut 의 결과는 Categorical 이고, categories 가 IntervalIndex 다.

import pandas as pd
s = pd.Series([5, 17, 25, 35, 55])
result = pd.cut(s, bins=[0, 18, 30, 50, 100])

# 각 구간의 IntervalIndex 확인
print(result.cat.categories)
# IntervalIndex([(0, 18], (18, 30], (30, 50], (50, 100]], dtype='interval[int64, right]')

# 구간 객체에서 left, right 추출
cats = result.cat.categories
print(cats[0].left, cats[0].right)   # 0, 18

# 직접 IntervalIndex 생성
intervals = pd.IntervalIndex.from_breaks([0, 18, 30, 50, 100], closed='right')
pd.cut(s, bins=intervals)

IntervalIndex 로 새 데이터 적용

# 훈련 데이터에서 bin 경계 학습
train_result, edges = pd.qcut(train['age'], q=5, retbins=True)

# 테스트 데이터에 같은 경계 적용
test_result = pd.cut(test['age'], bins=edges, labels=False)

ML 파이프라인에서 train/test 일관성 유지에 필수 패턴.

자주 쓰는 패턴

연령대별 통계

df['age_group'] = pd.cut(df['age'], bins=[0, 18, 30, 50, 100],
    labels=['미성년','청년','중년','노년'])
df.groupby('age_group', observed=True)['spending'].mean()

TIP

pandas 2.0 에서 groupby(observed=True) 를 명시하지 않으면 빈 카테고리도 포함해 집계한다는 경고가 발생한다. Categorical 컬럼을 groupby 할 때는 observed=True 권장.

동일 빈도 분위수 등급

df['percentile'] = pd.qcut(df['score'], q=10, labels=False)
# 0 ~ 9 의 십분위 라벨

사용자 정의 quantile

pd.qcut(df['price'], q=[0, 0.25, 0.5, 0.75, 0.9, 1.0],
    labels=['cheap','low','mid','high','luxury'])

ML feature engineering

import pandas as pd

# 연속형 피처 → 범주형 변환
df['age_bin'] = pd.cut(df['age'], bins=5, labels=False)  # 0-4 정수 라벨

# OneHot 인코딩 전처리
dummies = pd.get_dummies(df['age_bin'], prefix='age_bin')
df = pd.concat([df, dummies], axis=1)

구간별 분포 시각화

result = pd.cut(df['salary'], bins=10)
result.value_counts().sort_index().plot(kind='bar')
# 각 구간별 빈도 막대 그래프

성능 특성

항목내용
반환 타입Categorical (메모리 효율적)
내부 저장정수 codes + categories 배열
메모리object 대비 1/3~1/10 수준
연산 속도groupby, value_counts 에서 빠름
# Categorical 이 object 보다 메모리 효율적인 이유
s_obj = pd.Series(['child', 'adult', 'senior'] * 1_000_000)  # object
s_cat = s_obj.astype('category')                              # category

print(s_obj.memory_usage(deep=True))   # 약 192 MB
print(s_cat.memory_usage(deep=True))   # 약 3 MB (codes + categories)

함정

1. NaN 처리

pd.cut(s, bins=...)
# NaN 은 NaN 카테고리로 유지

2. duplicates (qcut)

pd.qcut(s, q=4)
# 데이터가 한 값에 몰려 있으면 quantile 경계 중복 → ValueError
pd.qcut(s, q=4, duplicates='drop')   # 중복 경계 자동 제거

데이터가 편향된 경우 qcut 이 실패할 수 있다. duplicates='drop' 으로 우선 처리 후 결과를 확인하라.

3. labels 의 길이

pd.cut(s, bins=[0, 18, 30, 50], labels=['a','b','c'])
# bins n+1 개, labels n 개
# 잘못 맞추면 ValueError

4. 새 데이터에 훈련 경계 미적용

# ❌ 잘못된 패턴: 훈련/테스트에 각각 qcut 적용
train['bin'] = pd.qcut(train['age'], q=4)  # 훈련 기준 경계
test['bin']  = pd.qcut(test['age'], q=4)   # 테스트 기준 경계 (다름!)

# ✓ 올바른 패턴: 훈련에서 edges 추출 후 test 에 cut 으로 적용
_, edges = pd.qcut(train['age'], q=4, retbins=True)
train['bin'] = pd.cut(train['age'], bins=edges, labels=False)
test['bin']  = pd.cut(test['age'],  bins=edges, labels=False)

5. groupby observed 경고 (pandas 2.0)

# ⚠️ FutureWarning: The default value of observed
df.groupby('age_group')['value'].sum()

# ✓ 명시적으로 지정
df.groupby('age_group', observed=True)['value'].sum()

관련 위키

이 글의 용어 (5개)
[Pandas] Categoricalpandas
정의 Categorical 은 제한된 고유값 만 가질 수 있는 dtype. 내부적으로 정수 코드로 저장되어 메모리 절약 과 속도 향상 을 동시에 달성한다. 고유값 수가 적고(lo…
[Pandas] DataFramepandas
정의 은 2차원 레이블 테이블. 각 열이 , 모든 열이 같은 (행 라벨) 를 공유. SQL 테이블 / Excel 시트 / R data.frame 의 Python 대응체. 구조 시…
[Pandas] get_dummies / from_dummiespandas
정의 는 범주형 컬럼을 원-핫 인코딩 (one-hot encoding) 한다. 범주 값 하나당 새 컬럼 (0/1 또는 True/False) 이 생성된다. ML 전처리의 가장 흔한…
[Pandas] groupbypandas
정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
[Pandas] value_countspandas
정의 는 각 고유값의 등장 빈도 를 내림차순 Series 로 반환. SQL 의 . pandas 1.1+ 부터는 도 가능 (여러 컬럼 조합). 기본 <CodeWithOutput l…

💬 댓글

사이트 검색 / 명령어

검색

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