Vectorization — for 루프를 버려야 하는 이유
이 토픽을 마치면
벡터화가 왜 빠른지 구조적으로 설명할 수 있고, for 루프 대신 NumPy/pandas의 벡터 연산으로 코드를 바꾸는 습관이 잡힙니다.
직관 — 같은 결과, 100배 차이
100만 개의 숫자를 제곱하는 두 가지 방법:
import numpy as npimport time
data = list(range(1_000_000))arr = np.array(data)
# 방법 1: for 루프start = time.time()result_loop = [x ** 2 for x in data]print(f"for 루프: {time.time() - start:.3f}초")
# 방법 2: NumPy 벡터 연산start = time.time()result_vec = arr ** 2print(f"벡터화: {time.time() - start:.3f}초")for 루프: 0.142초
벡터화: 0.002초 ← 약 70배 빠름결과는 동일합니다. 코드는 더 짧습니다. 속도는 수십 배 빠릅니다.
왜 이렇게 차이가 나는가
Python의 for 루프가 느린 이유는 매 반복마다 Python 인터프리터가 개입하기 때문입니다:
for 루프 (Python):
반복 1: 타입 체크 → 객체 생성 → 연산 → 객체 저장
반복 2: 타입 체크 → 객체 생성 → 연산 → 객체 저장
... (100만 번 반복)
벡터 연산 (NumPy):
한 번: C 코드가 메모리 연속 데이터를 일괄 처리 → 결과 배열 반환NumPy는 내부적으로 C/Fortran 코드로 루프를 돌립니다. Python 인터프리터의 오버헤드가 없고, 데이터가 메모리에 연속으로 저장되어 CPU 캐시를 효율적으로 사용합니다.
패턴 1 — 산술 연산
import numpy as np
prices = np.array([1000, 2500, 3000, 1500, 4000])tax_rate = 0.1
# ❌ for 루프total_loop = []for p in prices: total_loop.append(p * (1 + tax_rate))
# ✅ 벡터화total_vec = prices * (1 + tax_rate)print(total_vec) # [1100. 2750. 3300. 1650. 4400.]prices * (1 + tax_rate) — 배열 전체에 한 번에 연산합니다.
패턴 2 — 조건 필터링
scores = np.array([85, 42, 91, 67, 55, 78, 93, 38])
# ❌ for 루프passed = []for s in scores: if s >= 60: passed.append(s)
# ✅ 불리언 인덱싱passed = scores[scores >= 60]print(passed) # [85 91 67 78 93]scores >= 60은 [True, False, True, True, False, True, True, False] 불리언 배열을 만들고, 이것을 인덱스로 쓰면 True인 원소만 추출됩니다.
패턴 3 — pandas에서도 동일
import pandas as pd
df = pd.DataFrame({ "name": ["A", "B", "C", "D", "E"], "revenue": [500, 800, 300, 1200, 900], "cost": [200, 600, 150, 700, 400]})
# ❌ for 루프profits = []for _, row in df.iterrows(): profits.append(row["revenue"] - row["cost"])df["profit_loop"] = profits
# ✅ 벡터화df["profit_vec"] = df["revenue"] - df["cost"]df.iterrows()로 행을 하나씩 순회하는 것은 pandas에서 거의 항상 잘못된 접근입니다. 컬럼 단위 연산이 벡터화됩니다.
패턴 4 — 조건부 값 할당
# ❌ for 루프for i, row in df.iterrows(): if row["profit_vec"] > 500: df.at[i, "grade"] = "A" else: df.at[i, "grade"] = "B"
# ✅ np.wheredf["grade"] = np.where(df["profit_vec"] > 500, "A", "B")np.where(조건, 참일 때, 거짓일 때) — if/else를 배열 전체에 한 번에 적용합니다.
더 복잡한 다중 조건:
conditions = [ df["profit_vec"] > 500, df["profit_vec"] > 200, df["profit_vec"] > 0]choices = ["A", "B", "C"]df["grade"] = np.select(conditions, choices, default="D")벡터화할 수 없는 경우
모든 연산이 벡터화 가능한 것은 아닙니다:
# 이전 행의 값에 의존하는 누적 계산# (각 행이 이전 행 결과를 참조해야 해서 병렬화 불가)result = [data[0]]for i in range(1, len(data)): result.append(result[-1] + data[i])이런 경우에는 np.cumsum() 같은 전용 함수를 찾아보고, 없으면 for 루프를 쓰되 최소한의 범위에서만 사용합니다.
벤치마크 규칙
| 데이터 크기 | for 루프 | 벡터화 | 차이 |
|---|---|---|---|
| 1,000 | ~0.2ms | ~0.01ms | ~20x |
| 100,000 | ~20ms | ~0.1ms | ~200x |
| 10,000,000 | ~2s | ~20ms | ~100x |
데이터가 클수록 벡터화의 이점이 커집니다.
핵심 정리
| 상황 | 사용할 것 |
|---|---|
| 산술 연산 | arr * 2, arr + arr2 |
| 조건 필터 | arr[arr > 0] |
| 조건 할당 | np.where(), np.select() |
| 집계 | arr.sum(), arr.mean() |
| 행별 순회 | ❌ iterrows() 대신 컬럼 연산 |
apply() — 벡터화와 for 루프의 중간
벡터 연산으로 표현하기 어려운 복잡한 로직은 apply()를 사용합니다:
def categorize(row): if row["revenue"] > 1000 and row["profit_vec"] > 300: return "우수" elif row["profit_vec"] > 0: return "보통" else: return "적자"
df["category"] = df.apply(categorize, axis=1)apply()는 iterrows()보다 빠르지만 순수 벡터 연산보다는 느립니다. 복잡한 로직에는 apply(), 단순 연산에는 벡터화가 원칙입니다.
문자열 벡터화 — .str 접근자
names = pd.Series(["Alice Smith", "Bob Jones", "Charlie Brown"])
# ❌ for 루프upper_names = []for name in names: upper_names.append(name.upper())
# ✅ 벡터화upper_names = names.str.upper()first_names = names.str.split(" ").str[0]has_e = names.str.contains("e", case=False)
print(first_names) # ["Alice", "Bob", "Charlie"]pandas의 .str 접근자는 문자열 메서드를 벡터화합니다. split, replace, contains, extract 등 대부분의 문자열 연산을 for 루프 없이 처리합니다.
변환 리팩토링 체크리스트
기존 코드에서 for 루프를 발견하면 이 순서로 확인합니다:
- 산술/비교 연산인가? → 연산자 직접 사용 (
arr + 1,arr > 0) - 조건 분기인가? →
np.where()또는np.select() - 문자열 처리인가? →
.str접근자 - 집계인가? →
.sum(),.mean(),.groupby() - 복잡한 로직인가? →
apply() - 이전 행 의존인가? →
cumsum(),shift(), 전용 함수 탐색 - 위 모두 해당 없음 → for 루프 (최소 범위)
groupby — 그룹별 벡터 연산
df = pd.DataFrame({ "department": ["영업", "개발", "영업", "개발", "개발"], "salary": [5000, 6000, 4500, 7000, 5500]})
# ❌ for 루프로 부서별 평균departments = df["department"].unique()for dept in departments: mask = df["department"] == dept print(f"{dept}: {df.loc[mask, 'salary'].mean()}")
# ✅ groupby 벡터 연산print(df.groupby("department")["salary"].mean())groupby()는 그룹별로 벡터 연산을 적용합니다. 내부적으로 C 최적화되어 있어서 for 루프보다 빠르고 코드도 간결합니다.
dtype 주의사항
벡터 연산의 속도는 dtype에도 영향을 받습니다:
# object dtype (문자열 섞인 컬럼) → 벡터화 불가mixed = pd.Series([1, "two", 3]) # dtype: object → 느림clean = pd.Series([1, 2, 3]) # dtype: int64 → 빠름
# dtype 확인print(df.dtypes)# 숫자 컬럼이 object로 잡히면 변환 필요df["price"] = pd.to_numeric(df["price"], errors="coerce")CSV를 읽을 때 숫자 컬럼에 빈 문자열이 하나라도 있으면 전체가 object dtype이 되어 벡터 연산이 안 됩니다. pd.to_numeric()으로 변환 후 사용합니다.
"for 루프를 쓰기 전에, 벡터화된 방법이 있는지 먼저 확인한다." 이 습관 하나가 데이터 처리 성능을 크게 바꿉니다.