Skip to main content

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/DataFrameSeries/DataFrame범용 변환
map(func/dict)Series만Series원소별 매핑/변환
apply(func, axis=1)DataFrameSeries행 단위 연산

벡터화 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)은 내부적으로 각 행을 Python 객체로 변환한 후 함수를 호출합니다. 이 과정에서 NumPy의 벡터화 이점을 잃습니다. 행 수가 많을수록 오버헤드가 커집니다.
applymap()(Pandas 2.1+에서는 map())은 DataFrame의 모든 원소에 함수를 적용합니다. 전체 셀을 일괄 변환할 때 사용하지만, 벡터화 연산이 가능하면 그쪽이 더 빠릅니다.

체크리스트

  • apply, map의 차이를 설명할 수 있다
  • lambda와 apply를 조합하여 커스텀 변환을 작성할 수 있다
  • 벡터화 연산과 apply의 성능 차이를 이해한다
  • apply를 벡터화로 대체할 수 있는 패턴을 식별할 수 있다
  • 복잡한 행 단위 연산에서 apply(axis=1)을 사용할 수 있다

다음 문서