brandonwie.dev
EN / KR
On this page
backend backendpythonpandasperformance

pandas itertuples() vs iterrows()

`iterrows()`는 DataFrame 행을 순회하는 가장 흔한 방법이지만, 매 행마다 pd.Series 객체를 생성해서 느립니다

Updated March 22, 2026 3 min read

시간별로 실행되는 ETL 집계 작업이 10,000행을 처리하는 데 2초가 걸렸어요. 병목은 iterrows()였어요. itertuples()로 전환하니 20밀리초로 떨어졌어요 — 루프 시그니처 한 줄 바꾼 것치고 100배 개선이에요.

답답한 건 iterrows()가 모든 튜토리얼이 가르치는 방식이라는 거예요. Stack Overflow 답변들도 기본으로 이걸 사용해요. 심지어 일부 pandas 문서조차 기본 순회 패턴으로 이걸 쓰고 있어요. 극적으로 더 빠른 대안이 있다는 걸 발견하려면 의도적인 탐색이 필요했어요.

iterrows()가 느린 이유

iterrows()는 매 행마다 pd.Series 객체를 생성해요. Series 생성에는 타입 추론, 인덱스 생성, 메모리 할당이 들어가요. 값을 읽기만 하면 되는 상황에서 이 모든 작업이 낭비예요.

게다가 iterrows()는 dtype을 보존하지 않아요. 각 행을 공통 타입으로 캐스팅하는데, 정수가 실수가 되거나 타임스탬프가 object가 될 수 있어요. 사소한 불편함이 아니에요 — 다운스트림 로직에서 미묘한 버그를 일으킬 수 있어요.

해결책

대신 itertuples(index=False)를 사용하세요. 행당 오버헤드가 거의 없는 가벼운 namedtuple을 반환해요.

# BEFORE (iterrows)
for _, row in df.iterrows():
    val = row["column_name"]
    safe = row.get("column_name", default)

# AFTER (itertuples)
for row in df.itertuples(index=False):
    val = row.column_name
    safe = getattr(row, "column_name", default)

검토한 방법들

네 가지 접근법을 살펴봤어요:

방법장점단점
itertuples()~100배 빠름, 적은 메모리, 타입 보존attribute 접근만 가능, 불변 행
iterrows()dict 스타일 접근, 가변 Series, 익숙함~100배 느림, 많은 메모리, 타입 손실
apply()준벡터화, 유연함itertuples보다 ~10배 느림, 모호한 의미
벡터화 연산가장 빠름, pandas답움복잡한 행 로직에는 항상 가능하지 않음

벡터화 연산이 이상적이지만, 집계 로직에 열 단위 연산으로 표현할 수 없는 조건 분기가 있었어요. 행별 순회가 불가피했고, iterrows()itertuples() 사이의 선택이 결정적 요인이었어요.

apply()는 중간에 위치해요. iterrows()보다는 빠르지만 itertuples()보다 약 10배 느리고, 의미가 혼란스러울 수 있어요 — axis 파라미터에 따라 행 단위로 동작하기도 하고 열 단위로 동작하기도 해요.

주요 차이점

항목iterrows()itertuples()
반환값(index, Series)namedtuple
속도느림 (~1x)빠름 (~100x)
메모리높음 (행마다 Series)낮음 (namedtuple)
접근 방식row["col"] 또는 row.colrow.col만 가능
기본값 처리row.get("col", default)getattr(row, "col", default)
타입 보존아니오 (공통 타입으로 캐스팅)

마이그레이션 시 주의점

전환이 완전한 드롭인 교체는 아니에요. 세 가지가 걸렸어요:

접근 패턴이 바뀌어요. row["col"]은 namedtuple에서 동작하지 않아요. 모든 접근을 row.col 또는 getattr(row, "col")로 바꿔야 해요. 필드 접근이 수십 개인 큰 함수에서는 번거롭지만 기계적인 작업이에요.

.get()에 직접적인 대응이 없어요. Series의 row.get("col", default)는 namedtuple에서 getattr(row, "col", default)가 돼요. 같은 방식으로 동작하지만 다르게 읽히고 발견하기가 덜 쉬워요.

특수 문자가 포함된 컬럼명이 깨져요. DataFrame에 "event properties" (공백 포함) 같은 컬럼이 있으면 namedtuple attribute 접근이 실패해요. 먼저 컬럼명을 변경하거나 해당 경우에는 iterrows()로 대체해야 해요.

이게 왜 효과적인가

성능 차이는 객체 생성 비용에서 나와요. pd.Series는 인덱스, dtype 추론, 메모리 할당이 있는 무거운 객체예요. namedtuple은 가벼운 C 레벨 구조체예요. 10,000행을 순회할 때 Series 객체 10,000개를 만드는 것과 namedtuple 10,000개를 만드는 것의 차이가 2초와 20밀리초의 차이예요.

실제 예시

# schedule_changes_aggregation.py
# Processing ~10K daily events

# BEFORE: ~2s for 10K rows
for _, row in filtered.iterrows():
    event_props = row.get("event_properties", {})
    platform = row.get("platform")

# AFTER: ~20ms for 10K rows
for row in filtered.itertuples(index=False):
    event_props = getattr(row, "event_properties", {})
    platform = getattr(row, "platform", None)

참고로 성능 순위는 이래요:

vectorized ops  >>  itertuples()  >>  apply()  >>  iterrows()
   (fastest)          (~100x)         (~10x)        (1x baseline)

실전 가이드

itertuples()를 사용하면 좋은 경우:

  • 행별 순회가 불가피한 경우 (벡터화할 수 없는 복잡한 로직)
  • 행 값에 대한 읽기 전용 접근이면 충분한 경우
  • 컬럼명이 유효한 Python 식별자인 경우 (공백이나 특수 문자 없음)
  • 성능이 중요한 경우 (1,000행 이상에서 iterrows()가 눈에 띄게 느려짐)

iterrows()를 유지해도 되는 경우:

  • 컬럼명에 공백이나 특수 문자가 있는 경우: namedtuple attribute 접근은 유효한 Python 식별자가 필요해요. 먼저 컬럼을 이름 변경하거나 iterrows()로 대체하세요.
  • 행 값을 수정해야 하는 경우: namedtuple은 불변이에요. 제자리 변경이 필요하면 iterrows() 또는 벡터화 할당을 사용하세요.
  • 벡터화 연산이 가능한 경우: 로직을 열 단위 연산(.apply(), boolean indexing, .str accessor)으로 표현할 수 있다면 순회를 아예 건너뛰세요 — 어느 방법보다도 크기 차이로 빨라요.

어떤 순회 방법이든 손을 뻗기 전에, 로직을 벡터화할 수 있는지 먼저 물어보세요. 가능하다면 루프를 건너뛰세요. 불가능하다면 itertuples()를 사용하세요.

Comments

enko