apply와 Lambda
데이터 변환에서 내장 함수로 해결되지 않는 경우, apply()와 lambda를 사용하여 커스텀 함수를 적용할 수 있습니다. 그러나 가능하면 벡터화 연산을 우선 사용하는 것이 성능상 유리합니다.
학습 목표
- apply, map, applymap의 차이를 이해하고 적절히 사용할 수 있다
- lambda 함수와 apply를 조합하여 커스텀 변환을 수행할 수 있다
- 벡터화 연산과 apply의 성능 차이를 이해하고 최적의 방법을 선택할 수 있다
왜 중요한가
피처 엔지니어링에서는 기존 열을 조합하여 새로운 피처를 만드는 작업이 빈번합니다. 단순한 산술 연산은 벡터화로 해결되지만, 복잡한 비즈니스 로직이나 조건부 변환은 apply가 필요합니다. 올바른 도구를 선택하면 가독성과 성능을 모두 확보할 수 있습니다.
apply — 행/열에 함수 적용
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Kim', 'Lee', 'Park'],
'math': [85, 92, 78],
'english': [90, 88, 95]
})
# 열(Series)에 apply
df['math_grade'] = df['math'].apply(lambda x: 'A' if x >= 90 else 'B' if x >= 80 else 'C')
print(df['math_grade']) # ['B', 'A', 'C']
# 행(axis=1)에 apply — 여러 열 참조
def calc_total(row):
"""총점과 평균을 계산합니다."""
total = row['math'] + row['english']
return total
df['total'] = df.apply(calc_total, axis=1)
# 여러 값을 반환하는 apply
def stats(row):
"""평균과 합계를 함께 반환합니다."""
return pd.Series({
'total': row['math'] + row['english'],
'average': (row['math'] + row['english']) / 2
})
result = df.apply(stats, axis=1)
df = pd.concat([df, result], axis=1)
map — Series 원소별 변환
# 딕셔너리로 매핑
grade_map = {'A': 4.0, 'B': 3.0, 'C': 2.0}
df['gpa'] = df['math_grade'].map(grade_map)
# 함수로 매핑
df['name_lower'] = df['name'].map(str.lower)
# map은 매핑되지 않는 값을 NaN으로 처리
s = pd.Series(['a', 'b', 'c', 'd'])
result = s.map({'a': 1, 'b': 2}) # c, d는 NaN
| 메서드 | 입력 대상 | 반환 | 용도 |
|---|
apply(func) | Series/DataFrame | Series/DataFrame | 범용 변환 |
map(func/dict) | Series만 | Series | 원소별 매핑/변환 |
apply(func, axis=1) | DataFrame | Series | 행 단위 연산 |
벡터화 vs apply 성능
# 10만 행 데이터
df = pd.DataFrame({
'value': np.random.randn(100000)
})
# 방법 1: 벡터화 (가장 빠름)
%timeit df['value'] ** 2 + 2 * df['value'] + 1
# 약 0.5ms
# 방법 2: apply + lambda (느림)
%timeit df['value'].apply(lambda x: x**2 + 2*x + 1)
# 약 30ms (60배 느림)
# 방법 3: np.where (조건부 — 빠름)
%timeit np.where(df['value'] > 0, df['value'] * 2, 0)
# 약 0.5ms
# 방법 4: apply + 조건 (느림)
%timeit df['value'].apply(lambda x: x * 2 if x > 0 else 0)
# 약 40ms
우선순위: 벡터화 연산 > np.where/np.select > map > apply. apply는 벡터화로 해결할 수 없는 복잡한 로직에만 사용하세요.
벡터화 대체 패턴
# apply 대신 벡터화로 해결 가능한 예시들
# BAD: apply로 절대값
df['abs_val'] = df['value'].apply(lambda x: abs(x))
# GOOD: 벡터화
df['abs_val'] = df['value'].abs()
# BAD: apply로 조건부 값
df['label'] = df['value'].apply(lambda x: 'high' if x > 0.5 else 'low')
# GOOD: np.where
df['label'] = np.where(df['value'] > 0.5, 'high', 'low')
# BAD: apply로 문자열 처리
df['upper'] = df['name'].apply(lambda x: x.upper())
# GOOD: .str 접근자
df['upper'] = df['name'].str.upper()
AI/ML에서의 활용
- 피처 엔지니어링: 복잡한 도메인 로직을 apply로 적용하여 새 피처를 생성합니다
- 레이블 인코딩: map과 딕셔너리로 범주형 값을 숫자로 변환합니다
- 데이터 정제: apply로 복잡한 정제 규칙을 행 단위로 적용합니다
- 성능 최적화: 벡터화 가능한 연산을 식별하여 전처리 파이프라인을 가속합니다
apply(axis=1)이 느린 이유는 무엇인가요?
apply(axis=1)은 내부적으로 각 행을 Python 객체로 변환한 후 함수를 호출합니다. 이 과정에서 NumPy의 벡터화 이점을 잃습니다. 행 수가 많을수록 오버헤드가 커집니다.
applymap()(Pandas 2.1+에서는 map())은 DataFrame의 모든 원소에 함수를 적용합니다. 전체 셀을 일괄 변환할 때 사용하지만, 벡터화 연산이 가능하면 그쪽이 더 빠릅니다.
체크리스트
다음 문서