apply, map, applymap — 데이터 변환의 세 도구
이 토픽을 마치면
apply, map, applymap 세 함수의 차이를 명확히 구분하고, 상황에 맞는 도구를 선택할 수 있으며, 성능 차이를 이해합니다.
왜 세 개나 있는가
pandas에서 데이터를 변환하는 함수가 세 개 있어서 처음에는 혼란스럽습니다. 각각 적용 범위가 다릅니다.
| 함수 | 대상 | 적용 단위 |
|---|---|---|
map() | Series (1차원) | 개별 값 하나씩 |
apply() | Series 또는 DataFrame | 행 또는 열 단위 |
applymap() | DataFrame (2차원) | 개별 값 하나씩 |
한 문장으로: map은 Series의 각 값에, applymap은 DataFrame의 각 값에, apply는 행/열 전체에 함수를 적용합니다.
map — Series의 각 값을 변환
map()은 Series 전용입니다. 각 값에 함수를 적용하거나, 딕셔너리로 매핑합니다.
함수 매핑
import pandas as pd
df = pd.DataFrame({ "name": ["Alice", "Bob", "Carol"], "score": [85.7, 92.3, 78.1]})
# 각 이름을 대문자로df["name"].map(str.upper)# 0 ALICE# 1 BOB# 2 CAROL
# 각 점수를 반올림df["score"].map(round)# 0 86# 1 92# 2 78딕셔너리 매핑
grade_map = { "A": "Excellent", "B": "Good", "C": "Average"}
grades = pd.Series(["A", "B", "C", "A", "B"])grades.map(grade_map)# 0 Excellent# 1 Good# 2 Average# 3 Excellent# 4 Good딕셔너리에 없는 값은 NaN이 됩니다. 이것은 실무에서 카테고리 인코딩에 자주 사용됩니다.
lambda와 함께
df["score"].map(lambda x: "Pass" if x >= 80 else "Fail")# 0 Pass# 1 Pass# 2 Failapply — 행 또는 열 단위로 함수 적용
apply()는 Series와 DataFrame 모두에서 사용할 수 있습니다.
Series.apply — map과 비슷
df["score"].apply(lambda x: round(x, 1))# map()과 동일한 결과Series에서는 map()과 거의 같습니다. 차이는 apply()가 추가 인자를 넘길 수 있다는 정도입니다.
DataFrame.apply — 열(또는 행) 단위
df = pd.DataFrame({ "math": [85, 92, 78], "english": [90, 88, 95], "science": [88, 91, 82]})
# 각 과목(열)의 평균df.apply("mean") # axis=0 (기본값, 열 방향)# math 85.0# english 91.0# science 87.0
# 각 학생(행)의 평균df.apply("mean", axis=1) # axis=1 (행 방향)# 0 87.666667# 1 90.333333# 2 85.000000axis=0은 "위에서 아래로" (각 열에 함수 적용), axis=1은 "왼쪽에서 오른쪽으로" (각 행에 함수 적용)입니다.
행 단위로 여러 컬럼 참조
df = pd.DataFrame({ "name": ["Alice", "Bob", "Carol"], "math": [85, 92, 78], "english": [90, 88, 95]})
# 각 학생의 두 과목 중 높은 점수df.apply(lambda row: max(row["math"], row["english"]), axis=1)# 0 90# 1 92# 2 95행(axis=1) 단위 apply에서 row는 해당 행의 Series입니다. 여러 컬럼을 참조해서 복합 계산을 할 수 있습니다.
applymap — DataFrame의 모든 값을 변환
applymap()은 DataFrame 전용이며, 모든 셀에 함수를 적용합니다.
df = pd.DataFrame({ "math": [85.7, 92.3, 78.1], "english": [90.2, 88.9, 95.4], "science": [88.1, 91.7, 82.3]})
# 모든 값을 정수로df.applymap(int)# math english science# 0 85 90 88# 1 92 88 91# 2 78 95 82
# 모든 값에 포맷 적용df.applymap(lambda x: f"{x:.1f}%")# math english science# 0 85.7% 90.2% 88.1%# 1 92.3% 88.9% 91.7%# 2 78.1% 95.4% 82.3%참고: pandas 2.1+에서
applymap()은map()으로 통합되었습니다.DataFrame.map()으로 같은 작업을 할 수 있습니다. 하지만 많은 코드베이스에서 아직applymap()을 사용하므로 알아둘 필요가 있습니다.
세 함수 비교 — 한눈에
import pandas as pd
df = pd.DataFrame({ "A": [1, 2, 3], "B": [4, 5, 6]})
# map: Series 각 값 → 값df["A"].map(lambda x: x * 10) # Series → Series# 0 10# 1 20# 2 30
# apply (Series): map과 비슷df["A"].apply(lambda x: x * 10) # Series → Series (동일 결과)
# apply (DataFrame, axis=0): 열 단위df.apply(sum) # DataFrame → Series# A 6# B 15
# apply (DataFrame, axis=1): 행 단위df.apply(sum, axis=1) # DataFrame → Series# 0 5# 1 7# 2 9
# applymap: DataFrame 각 값 → 값df.applymap(lambda x: x * 10) # DataFrame → DataFrame# A B# 0 10 40# 1 20 50# 2 30 60성능 — 벡터 연산을 먼저
apply, map, applymap은 내부적으로 Python 루프를 돕니다. pandas의 내장 연산(벡터 연산)이 훨씬 빠릅니다.
import numpy as np
df = pd.DataFrame({"value": range(1_000_000)})
# Slow — apply (Python 루프)%timeit df["value"].apply(lambda x: x * 2)# ~200ms
# Fast — 벡터 연산 (C로 실행)%timeit df["value"] * 2# ~2ms (100배 빠름)규칙: 단순 산술, 비교, 문자열 메서드는 벡터 연산을 씁니다. apply()는 벡터 연산으로 표현할 수 없는 복잡한 로직에서만 사용합니다.
# Bad — apply로 조건 분기df["label"] = df["value"].apply(lambda x: "high" if x > 500000 else "low")
# Good — np.where (벡터 연산)df["label"] = np.where(df["value"] > 500000, "high", "low")
# Bad — apply로 문자열 처리df["upper"] = df["name"].apply(str.upper)
# Good — str accessor (벡터 연산)df["upper"] = df["name"].str.upper()실전 패턴 — 복합 변환
벡터 연산으로 불가능한 경우에 apply를 씁니다:
df = pd.DataFrame({ "name": ["Alice Smith", "Bob Lee", "Carol Park"], "birth": ["1995-03-15", "1988-11-22", "2001-07-08"], "department": ["Sales", "Dev", "HR"]})
def create_employee_id(row): dept_code = row["department"][:2].upper() last_name = row["name"].split()[-1].upper() year = row["birth"][:4] return f"{dept_code}-{last_name}-{year}"
df["emp_id"] = df.apply(create_employee_id, axis=1)print(df["emp_id"])# 0 SA-SMITH-1995# 1 DE-LEE-1988# 2 HR-PARK-2001여러 컬럼을 참조해서 복잡한 문자열을 만드는 작업 — 이것은 벡터 연산으로 표현하기 어렵고, apply(axis=1)이 적절합니다.
핵심 정리
| 상황 | 도구 |
|---|---|
| Series 각 값 변환 | map() 또는 apply() |
| 딕셔너리로 값 매핑 | map(dict) |
| DataFrame 각 셀 변환 | applymap() (또는 pandas 2.1+ map()) |
| 열 단위 집계 | apply(func, axis=0) |
| 행 단위 복합 계산 | apply(func, axis=1) |
| 단순 산술/비교 | 벡터 연산 (apply 쓰지 말 것) |
세 함수를 고르는 순서: 1) 벡터 연산으로 되는가? → 벡터 연산. 2) Series 각 값? → map. 3) DataFrame 각 셀? → applymap. 4) 행/열 단위 복합 로직? → apply. 이 순서를 기억하면 성능과 가독성 모두 챙길 수 있습니다.
자주 하는 실수
| 실수 | 문제 | 해결 |
|---|---|---|
df.map(func) (pandas < 2.1) | AttributeError | df.applymap(func) 또는 pandas 업그레이드 |
df.apply(func) 에 스칼라 반환 | 기대와 다른 결과 | axis 확인 — 0은 열, 1은 행 |
| 단순 산술에 apply 사용 | 100배 느림 | 벡터 연산으로 교체 |
| map에 없는 키 | NaN 발생 | fillna()로 기본값 지정 |