0Pricing
Pandas & NumPy Academy · 강의

iterrows와 Python 반복문 피하기

행 단위 반복문을 벡터화된 열 연산, np.where, pd.cut으로 바꿔 10~100배의 속도 향상을 얻습니다.

iterrows와 Python 반복문 피하기은(는) CoddyKit의 무료 Pandas & NumPy Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Pandas & NumPy Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Pandas & NumPy Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

행 단위 반복의 비용

Pandas는 NumPy를 기반으로 하며, 컴파일된 C 코드를 사용하여 전체 배열을 한 번에 처리합니다. for _, row in df.iterrows()처럼 행 단위로 반복하면 이러한 장점을 활용하지 못하고 순수 Python으로 되돌아갑니다. 각 행이 Series 객체로 추출되고, 반복문은 Python 인터프리터에서 실행되므로 동일한 벡터화 작업에 비해 비용이 약 100~1000배 더 큽니다. 100만 행의 DataFrame에서는 이 차이로 인해 밀리초가 아니라 몇 분이 걸릴 수도 있습니다.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

# Slow: iterrows loop
def loop_version(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] + row['b'])
    return pd.Series(result)

# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)

print(f'Loop (5 runs):       {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')

조건부 반복을 np.where로 바꾸기

np.where(condition, value_if_true, value_if_false)는 요소별 if/else에 해당하는 벡터화 방식입니다. 행을 반복하며 if 문을 작성하는 대신 조건과 두 결과 값을 배열로 전달하십시오. np.where는 전체 열을 한 번에 C 코드로 계산하므로, 큰 DataFrame에서는 동일한 Python 반복문보다 50~200배 빠릅니다.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})

# Slow: iterrows
def label_loop(df):
    labels = []
    for _, row in df.iterrows():
        if row['price'] > 50:
            labels.append('expensive')
        else:
            labels.append('cheap')
    return labels

# Fast: np.where
def label_vectorised(df):
    return np.where(df['price'] > 50, 'expensive', 'cheap')

# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])

np.select를 사용한 다중 조건

조건이 세 개 이상이면 중첩된 np.where 대신 np.select(condlist, choicelist, default=)를 사용하십시오. 불리언 배열의 목록과 그에 대응하는 출력 값의 목록을 전달합니다. 처음으로 일치하는 조건이 출력을 결정하고, 일치하지 않는 행에는 default 값이 지정됩니다. 이렇게 하면 반복문 안의 복잡한 if/elif/else 연쇄를 깔끔한 벡터화 표현식으로 바꿀 수 있습니다.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})

conditions = [
    df['score'] >= 90,
    df['score'] >= 75,
    df['score'] >= 60
]
choices = ['A', 'B', 'C']

df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))

.str을 통한 벡터화된 문자열 작업

문자열 조작은 느린 반복문이 발생하는 흔한 원인입니다. Pandas의 .str 접근자는 모든 Python 문자열 메서드에 해당하는 벡터화 기능을 제공합니다. 예를 들어 .str.lower(), .str.strip(), .str.replace(), .str.contains() 등이 있습니다. .str 메서드를 사용하면 행을 반복하며 Python 문자열 메서드를 직접 호출하는 것보다 10~50배 빠릅니다.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', '  carol  ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})

# Slow: loop
def clean_loop(df):
    return [n.strip().title() for n in df['name']]

# Fast: .str accessor
def clean_vectorised(df):
    return df['name'].str.strip().str.title()

t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)

print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())

반복문 대신 pd.cut과 pd.qcut 사용하기

pd.cut()과 pd.qcut()은 여러 if/elif 조건을 사용하는 반복문으로 작성하는 경우가 많은 구간 나누기 작업을 벡터화합니다. 행 반복문 안에 'if value < 18: age_group = child elif value < 65: age_group = adult'를 작성하고 있다면, 전체 열을 C 속도로 한 번에 처리하는 단일 pd.cut() 호출로 바꾸십시오.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})

# Slow: loop with conditionals
def categorise_loop(df):
    result = []
    for age in df['age']:
        if age < 18:
            result.append('child')
        elif age < 65:
            result.append('adult')
        else:
            result.append('senior')
    return result

# Fast: pd.cut
def categorise_cut(df):
    return pd.cut(df['age'], bins=[0, 18, 65, 100],
                  labels=['child', 'adult', 'senior'],
                  right=False)

assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())

apply()가 항상 정답은 아닙니다

많은 튜토리얼에서는 반복문을 .apply(func)로 바꾸라고 권장하지만, apply도 내부적으로는 Python 수준의 반복문입니다. 따라서 iterrows보다 조금 빠를 뿐이며, 진정한 벡터화보다는 훨씬 느립니다. 벡터화된 대안이 없는 경우(복잡한 다중 열 논리)에만 apply를 사용하십시오. 내장 Pandas 또는 NumPy 함수로 작업을 표현할 수 있다면 항상 apply보다 해당 함수를 우선하십시오.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})

# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)

print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')

groupby 반복문을 agg와 transform으로 바꾸기

흔히 사용하는 패턴 중 하나는 그룹을 직접 반복하는 것입니다. for name, group in df.groupby('cat'): do_something(group)처럼 작성하는 방식입니다. 이는 iterrows가 느린 것과 같은 이유로 느립니다. 요약 통계를 만들 때는 groupby().agg()로 바꾸고, 그룹 수준의 통계를 원래 행 위치에 브로드캐스트할 때는 groupby().transform()으로 바꾸십시오.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'cat': np.random.choice(['A','B','C','D'], 100000),
    'val': np.random.randn(100000)
})

# Slow: manual loop to add group mean
def slow_group_mean(df):
    means = {}
    for name, group in df.groupby('cat'):
        means[name] = group['val'].mean()
    return df['cat'].map(means)

# Fast: transform
def fast_group_mean(df):
    return df.groupby('cat')['val'].transform('mean')

t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop:       {t1/10*1000:.1f} ms')
print(f'transform:  {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')

iterrows를 사용해도 괜찮은 경우

느리기는 하지만 iterrows와 itertuples를 사용해야 하는 정당한 경우도 있습니다. 행의 정보를 출력하거나 logging하는 경우(성능이 중요하지 않음), API 요청 데이터를 구성하는 경우(각 행이 HTTP 호출 하나를 발생시켜 네트워크 지연이 지배적임), 복잡한 조건으로 특정 행을 debugging하는 경우가 이에 해당합니다. 행이 1,000개 미만이고 작업을 한 번만 실행한다면 반복문과 벡터화의 차이는 밀리초 수준이므로, 코드 복잡성을 높일 가치가 없습니다.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'order_id': [101, 102, 103],
    'product': ['Widget', 'Gadget', 'Doohickey'],
    'amount': [29.99, 49.99, 9.99]
})

# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
    print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')

itertuples가 iterrows보다 빠릅니다

Python에서 행을 반복해야 한다면(벡터화된 대안이 없기 때문이라면) iterrows() 대신 itertuples()를 사용하십시오. 각 행을 Pandas Series가 아니라 이름이 지정된 튜플로 반환하므로 Series를 만드는 데 드는 비용을 피할 수 있습니다. 따라서 일반적으로 iterrows보다 5~10배 빠릅니다. 속성 표기법으로 값에 접근하십시오: row.column_name. 열 이름에 공백이 있다면 유효한 속성 이름이 아니므로 사용하지 마십시오.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

t1 = timeit.timeit(
    lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
    lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)

print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows:   {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')

벡터화 패턴 점검표

반복문을 작성하기 전에 다음 질문을 확인하십시오.

  • 한 열에 대해 요소별 작업인가요? → 열 산술 표현식 또는 NumPy ufunc를 사용하십시오.
  • 조건부 논리가 포함되나요? → np.where(조건 2개) 또는 np.select(조건 3개 이상)를 사용하십시오.
  • 값을 구간으로 나누나요? → pd.cut 또는 pd.qcut을 사용하십시오.
  • 문자열 작업을 적용하나요? → .str 접근자를 사용하십시오.
  • 그룹 내에서 집계하나요? → groupby().agg() 또는 groupby().transform()을 사용하십시오.
위 방법 중 어느 것도 적용되지 않을 때만 apply() 또는 itertuples()로 대체하십시오.

실제 속도 향상 예시: 가격 계산

주문 수량에 따라 단계별 할인을 적용하는 일반적인 비즈니스 계산을 리팩터링하기 전과 후의 전체 예시입니다. 반복문 버전은 읽기 쉽지만 큰 DataFrame에서는 허용할 수 없을 정도로 느립니다. np.select를 사용하는 벡터화 버전은 동일한 결과를 10분의 1 시간에 계산합니다.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'price': np.random.uniform(10, 200, 100000),
    'qty': np.random.randint(1, 500, 100000)
})

# BEFORE: loop with conditionals
def slow_discount(df):
    result = []
    for _, row in df.iterrows():
        if row['qty'] >= 100:
            disc = 0.2
        elif row['qty'] >= 50:
            disc = 0.1
        elif row['qty'] >= 10:
            disc = 0.05
        else:
            disc = 0.0
        result.append(row['price'] * (1 - disc))
    return pd.Series(result)

# AFTER: np.select
def fast_discount(df):
    conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
    discounts = [0.20, 0.10, 0.05]
    disc = np.select(conditions, discounts, default=0.0)
    return df['price'] * (1 - disc)

assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')

빠른 확인

이 lesson에서 배운 벡터화에 대한 이해도를 확인해 보십시오.

lesson 복습

이 lesson에서는 다음을 배웠습니다. iterrows는 벡터화 작업보다 100~1000배 느리고, np.where와 np.select는 조건부 반복문을 대체하며, .str 접근자는 문자열 작업을 벡터화하고, groupby().transform()은 수동 그룹 반복을 대체합니다. 반복이 반드시 필요하다면 iterrows보다 itertuples를 사용하여 5~10배의 성능 향상을 얻으십시오. 다음에서는 효율적인 데이터 형식, 즉 숫자형을 다운캐스팅하고 Categorical을 사용하여 메모리를 최대 70%까지 줄이는 방법을 살펴봅니다.

자주 묻는 질문

“iterrows와 Python 반복문 피하기” 강의는 무료인가요?

네 — “iterrows와 Python 반복문 피하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Pandas & NumPy Academy 강의 전체를 잠금 해제할 수 있습니다. Pandas & NumPy Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“iterrows와 Python 반복문 피하기”에서 뭘 배우나요?

행 단위 반복문을 벡터화된 열 연산, np.where, pd.cut으로 바꿔 10~100배의 속도 향상을 얻습니다. 브라우저에서 직접 실행하는 실습 코드로 Pandas & NumPy Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Pandas & NumPy Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Pandas & NumPy Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“iterrows와 Python 반복문 피하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Pandas & NumPy Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Pandas & NumPy Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. timeit과 memory_profiler로 프로파일링하기
  2. iterrows와 Python 반복문 피하기
  3. 메모리 절약을 위한 효율적인 데이터 형식
  4. 대용량 파일 청크 단위 읽기
← Pandas & NumPy Academy(으)로 돌아가기