BioPlayground

🧬
목록으로

Vectorization — for 루프를 버려야 하는 이유

NumPy/pandas의 벡터화 연산이 for 루프보다 수십~수백 배 빠른 이유를 코드와 벤치마크로 설명합니다.

중급
|
10
|
검증 완료 (2026-07)
벡터화vectorizationnumpy 성능for 루프 제거배열 연산
진행률0/17 (0%)

Vectorization — for 루프를 버려야 하는 이유

이 토픽을 마치면

벡터화가 왜 빠른지 구조적으로 설명할 수 있고, for 루프 대신 NumPy/pandas의 벡터 연산으로 코드를 바꾸는 습관이 잡힙니다.


직관 — 같은 결과, 100배 차이

100만 개의 숫자를 제곱하는 두 가지 방법:

python
import numpy as np
import 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 ** 2
print(f"벡터화: {time.time() - start:.3f}초")
text
for 루프: 0.142초
벡터화:   0.002초    ← 약 70배 빠름

결과는 동일합니다. 코드는 더 짧습니다. 속도는 수십 배 빠릅니다.


왜 이렇게 차이가 나는가

Python의 for 루프가 느린 이유는 매 반복마다 Python 인터프리터가 개입하기 때문입니다:

text
for 루프 (Python):
  반복 1: 타입 체크 → 객체 생성 → 연산 → 객체 저장
  반복 2: 타입 체크 → 객체 생성 → 연산 → 객체 저장
  ... (100만 번 반복)

벡터 연산 (NumPy):
  한 번: C 코드가 메모리 연속 데이터를 일괄 처리 → 결과 배열 반환

NumPy는 내부적으로 C/Fortran 코드로 루프를 돌립니다. Python 인터프리터의 오버헤드가 없고, 데이터가 메모리에 연속으로 저장되어 CPU 캐시를 효율적으로 사용합니다.


패턴 1 — 산술 연산

python
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 — 조건 필터링

python
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에서도 동일

python
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 — 조건부 값 할당

python
# ❌ for 루프
for i, row in df.iterrows():
if row["profit_vec"] > 500:
df.at[i, "grade"] = "A"
else:
df.at[i, "grade"] = "B"
# ✅ np.where
df["grade"] = np.where(df["profit_vec"] > 500, "A", "B")

np.where(조건, 참일 때, 거짓일 때) — if/else를 배열 전체에 한 번에 적용합니다.

더 복잡한 다중 조건:

python
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")

벡터화할 수 없는 경우

모든 연산이 벡터화 가능한 것은 아닙니다:

python
# 이전 행의 값에 의존하는 누적 계산
# (각 행이 이전 행 결과를 참조해야 해서 병렬화 불가)
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()를 사용합니다:

python
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 접근자

python
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 루프를 발견하면 이 순서로 확인합니다:

  1. 산술/비교 연산인가? → 연산자 직접 사용 (arr + 1, arr > 0)
  2. 조건 분기인가?np.where() 또는 np.select()
  3. 문자열 처리인가?.str 접근자
  4. 집계인가?.sum(), .mean(), .groupby()
  5. 복잡한 로직인가?apply()
  6. 이전 행 의존인가?cumsum(), shift(), 전용 함수 탐색
  7. 위 모두 해당 없음 → for 루프 (최소 범위)

groupby — 그룹별 벡터 연산

python
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에도 영향을 받습니다:

python
# 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 루프를 쓰기 전에, 벡터화된 방법이 있는지 먼저 확인한다." 이 습관 하나가 데이터 처리 성능을 크게 바꿉니다.