메뉴 닫기

파이썬 pandas 성능 최적화 벡터화 우선 가이드 apply iterrows 지양 itertuples 대안과 numexpr query eval 활용

파이썬 pandas 성능 최적화 벡터화 우선 가이드 apply iterrows 지양 itertuples 대안과 numexpr query eval 활용

⚡ 벡터화로 속도 높이고 병목 줄이는 데이터프레임 최적화 비법을 한 번에 정리합니다

데이터 전처리와 집계를 하다 보면 코드가 돌아가는 데 시간이 너무 오래 걸려서 손이 묶이는 순간이 생깁니다.
그런데 병목의 상당 부분은 문법이나 알고리즘이 아니라 pandas를 쓰는 방식에서 비롯됩니다.
특히 반복문 스타일의 처리나 행 단위 연산은 파이썬 인터프리터 레벨에서 오버헤드를 유발해 성능을 크게 떨어뜨립니다.
속도를 끌어올리는 가장 확실한 방법은 벡터화를 우선하고, 가능한 한 파이썬 레벨의 루프를 피하는 것입니다.
오늘 글에서는 실제 작업 흐름에서 바로 적용할 수 있도록 원칙과 대안을 정리해 드립니다.
읽고 나면 어떤 경우에 apply와 iterrows를 지양해야 하는지, itertuples를 어떻게 써야 안전한지, 그리고 numexpr를 통해 query·eval을 활용해 계산을 더 빠르게 만드는 요령까지 감을 잡을 수 있을 거예요.

핵심은 다음과 같습니다.
첫째, 연산을 배열 단위로 넘겨서 C 레벨 최적화를 활용하는 벡터화 전략이 기본값이 되어야 합니다.
둘째, 파이썬 함수 호출이 반복되는 apply와 파이썬 객체를 반환하는 iterrows는 비용이 커서 지양합니다.
셋째, 행 접근이 꼭 필요하다면 튜플 기반으로 빠르고 가벼운 itertuples가 대안이 됩니다.
넷째, 수치 표현식은 numexpr 엔진으로 평가하면 멀티스레드와 효율적 메모리 사용 이점을 얻어 query와 eval에서 가속이 가능합니다.
이 글은 위 원칙을 실제 코드와 체크리스트 중심으로 풀어 효율적인 데이터 파이프라인을 설계하도록 돕습니다.



🚀 파이썬 pandas 성능 최적화 핵심 원칙 벡터화 우선

pandas에서 처리 속도를 좌우하는 가장 큰 요인은 연산을 어디에서 수행하느냐입니다.
파이썬 레벨에서 루프를 돌리면 함수 호출 오버헤드와 객체 생성 비용이 누적되어 속도가 크게 떨어집니다.
반면, 벡터화는 내부적으로 C 레벨의 배열 연산을 활용하므로 메모리 접근이 연속적이고 브로드캐스팅 최적화 이점을 얻습니다.
따라서 기본 전략은 가능한 모든 연산을 시리즈 또는 데이터프레임 전체에 한 번에 적용하는 것입니다.
조건 분기, 수치 계산, 문자열 처리, 집계 등에서 먼저 벡터화 표현을 떠올리면 대부분의 병목을 해소할 수 있습니다.

행 단위 처리 도구인 applyiterrows는 파이썬 객체를 오가며 호출 비용이 크기 때문에 지양합니다.
불가피하게 행 순회를 해야 한다면 튜플 기반으로 가벼운 itertuples가 상대적으로 효율적입니다.
또한 수치 계산식은 numexpr 엔진을 활용하는 DataFrame.queryeval로 평가하면 내부적으로 최적화되어 큰 데이터에서 가속을 기대할 수 있습니다.
이 원칙은 단순한 팁이 아니라, 데이터 파이프라인 전반의 설계 기준으로 삼을 만한 기본값입니다.

  • 먼저 벡터화 가능한지 점검하고, 배열 연산으로 표현한다.
  • 🚫applyiterrows는 지양한다. 불가피하면 itertuples를 고려한다.
  • 🧮수치식은 eval·query로 작성해 numexpr 최적화를 활용한다.
  • 🧪작은 예제로 먼저 맞는지 검증하고, 동일한 표현을 전체 데이터에 확장한다.
CODE BLOCK
# 느린 패턴: apply로 파이썬 함수 반복 호출
df["z"] = df.apply(lambda r: r["x"] * 1.2 + r["y"] ** 2, axis=1)

# 권장 패턴: 벡터화
df["z"] = df["x"] * 1.2 + df["y"] ** 2

# 대안: numexpr 가속 (eval/query)
# 설정: pd.set_option("compute.use_numexpr", True) 가 기본 활성화인 환경이 많음
df.eval("z = x * 1.2 + y ** 2", inplace=True)

# 불가피한 행 순회일 때: itertuples (iterrows 지양)
total = 0.0
for row in df.itertuples(index=False):
    total += row.x * 1.2 + row.y ** 2

💎 핵심 포인트:
벡터화를 기본값으로 두고, 행 단위 처리는 마지막 수단으로만 채택합니다.
수치식은 eval과 query로 옮겨 numexpr의 멀티스레드 최적화 이점을 얻습니다.

⚠️ 주의: iterrows는 각 행을 파이썬 Series로 만들어 반환하므로 메모리와 시간 비용이 큽니다.
같은 로직이라도 결과 타입과 성능이 달라질 수 있으니 일관성 있는 자료형 관리와 벡터화 우선 원칙을 유지하세요.

🔁 apply iterrows 대신 무엇을 쓸까 itertuples 전략

pandas에서 반복문을 돌릴 때 가장 흔한 패턴은 DataFrame.apply()iterrows()입니다.
하지만 이 두 방식 모두 내부적으로 각 행을 파이썬 객체로 변환하여 처리하기 때문에, 데이터가 수만 건만 되어도 실행 속도가 기하급수적으로 느려집니다.
이런 성능 저하를 피하려면 반드시 벡터화가 가능한지 먼저 확인하고, 불가피한 경우에만 itertuples()을 고려해야 합니다.

⚙️ apply와 iterrows의 한계

apply는 각 행에 사용자가 정의한 함수를 적용할 때마다 파이썬 함수 호출이 일어나고, 그 결과를 새 시리즈로 반환합니다.
이는 CPU 캐시 비효율과 객체 생성 오버헤드를 유발해 대용량 데이터에서 매우 느리게 동작합니다.
iterrows 역시 각 행을 Series 객체로 생성하므로 메모리 점유율이 높고, 타입 일관성이 깨질 수 있습니다.
특히 dtype이 float64나 int64에서 object로 변환되면 이후 연산이 더 느려지는 부작용도 있습니다.

🚀 itertuples로 대체하는 방법

itertuples()은 각 행을 파이썬 네임드튜플(namedtuple) 형태로 반환하기 때문에 속도가 훨씬 빠르고, 메모리 사용량도 적습니다.
각 열을 속성으로 접근할 수 있고, 반환 타입이 고정되어 있어 타입 변환 문제가 없습니다.
다만 데이터가 수백만 행 단위로 커지면, 여전히 파이썬 루프 오버헤드가 남으므로 가능한 한 브로드캐스팅 방식으로 옮기는 것이 최선입니다.

CODE BLOCK
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "x": np.random.rand(1_000_000),
    "y": np.random.rand(1_000_000)
})

# 느린 방식
df["z"] = df.apply(lambda r: r["x"] * r["y"] + 2, axis=1)

# 빠른 대안
df["z"] = df["x"] * df["y"] + 2

# 필요 시 itertuples
total = 0
for row in df.itertuples(index=False):
    total += row.x * row.y + 2

💎 핵심 포인트:
apply와 iterrows는 편리하지만 느리고, 대용량 환경에서는 비효율적입니다.
itertuples는 가볍고 빠르지만, 여전히 루프 기반이라 벡터화와 혼합해 사용해야 합니다.

방식 특징 속도
apply 함수 호출마다 오버헤드 발생, 타입 변환 잦음 매우 느림
iterrows Series 단위 반환, dtype 일관성 깨짐 느림
itertuples NamedTuple 반환, 타입 안정적 상대적으로 빠름
벡터화 배열 단위 C 레벨 연산 가장 빠름

⚠️ 주의: itertuples는 빠르지만 여전히 파이썬 루프입니다.
대량 연산을 반복적으로 수행하면 GIL의 제약을 받으므로, 가능하면 NumPy 연산이나 pandas의 내장 함수로 대체하세요.



🧮 브로드캐스팅과 벡터화 실전 패턴

pandas의 진짜 강점은 브로드캐스팅(broadcasting)벡터화(vectorization) 기능에 있습니다.
이는 NumPy의 핵심 원리를 pandas가 계승한 것으로, 단일 값 또는 배열을 전체 시리즈에 자동으로 확장해 연산할 수 있는 방식입니다.
즉, 각 원소에 대해 반복문을 돌리지 않아도 내부적으로는 C 레벨에서 연속된 메모리 연산이 수행되어 처리 속도가 수십 배 이상 빨라집니다.

예를 들어 ‘모든 값에 일정한 계수를 곱하고 특정 조건에서 다른 연산을 수행한다’는 작업도 루프 없이 표현할 수 있습니다.
이는 단순한 코드 단축 이상의 의미를 갖습니다.
루프를 없애면 캐시 효율이 높아지고, 병렬 벡터 연산이 활성화되며, 파이썬의 GIL(Global Interpreter Lock) 병목을 피할 수 있습니다.
결과적으로 더 짧고 명확한 코드로 더 빠른 실행 속도를 얻는 것이죠.

CODE BLOCK
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "price": [100, 250, 400, 150],
    "discount": [0.1, 0.25, 0.3, 0.15]
})

# 벡터화된 연산: 모든 행에 동시에 계산
df["final"] = df["price"] * (1 - df["discount"])

# 조건부 연산도 벡터화로 처리 가능
df["category"] = np.where(df["final"] > 200, "Premium", "Standard")

# 브로드캐스팅으로 상수값 더하기
df["taxed"] = df["final"] + 5
print(df)

💡 TIP: 조건문은 np.where()masking으로 표현하면 if-else 루프보다 훨씬 효율적입니다.
단순한 비교·대입 작업이라면 벡터화만으로 충분히 처리 가능합니다.

📊 그룹 연산에서도 벡터화 가능

groupby나 agg를 사용할 때에도 루프를 쓰지 말고, 집계 함수를 전달하는 것이 중요합니다.
각 그룹에 대해 직접 for문을 돌리면 속도가 급격히 저하됩니다.
내장 함수 sum, mean, max, min, size 등을 전달하면 내부적으로 벡터화된 C 코드가 실행됩니다.

CODE BLOCK
df = pd.DataFrame({
    "group": ["A", "A", "B", "B", "C"],
    "value": [10, 15, 20, 25, 30]
})

# 느린 방식: 수동 루프 집계
result = {}
for g in df["group"].unique():
    subset = df[df["group"] == g]
    result[g] = subset["value"].mean()

# 빠른 방식: 내장 집계 함수 사용
df.groupby("group")["value"].mean()

💎 핵심 포인트:
벡터화는 단순히 for문을 없애는 것이 아니라, pandas 내부의 C 엔진을 활용해 병목을 줄이는 근본적인 접근입니다.
데이터 처리 성능을 개선하려면 브로드캐스팅 가능 여부를 항상 먼저 확인하세요.

⚙️ numexpr로 가속하는 query eval 활용법

pandas에는 numexpr이라는 고성능 연산 엔진이 내장되어 있습니다.
이는 수치 연산을 파이썬 인터프리터 대신 C 기반 엔진에서 병렬로 처리하기 때문에, 복잡한 산술식이나 필터링 작업을 훨씬 빠르게 실행할 수 있습니다.
이 엔진은 DataFrame.query()DataFrame.eval() 메서드를 통해 자동으로 활용됩니다.

특히 대규모 데이터셋에서 여러 열을 동시에 계산해야 할 때, numexpr는 불필요한 중간 객체 생성을 줄이고, CPU 캐시 활용률을 높여 성능을 극대화합니다.
이 방식은 단순히 가독성 향상뿐 아니라, 수천만 행 단위 연산에서 수초 단위의 실행 속도 차이를 만들어 냅니다.

CODE BLOCK
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "a": np.random.rand(10_000_000),
    "b": np.random.rand(10_000_000),
    "c": np.random.rand(10_000_000)
})

# 일반 방식: 중간 객체가 계속 생김
df["result"] = (df["a"] + df["b"]) / df["c"]

# numexpr 기반: 메모리 효율적이고 빠름
df.eval("result = (a + b) / c", inplace=True)

# 조건 필터링도 query로 가능
filtered = df.query("a > 0.5 & b < 0.3")

# numexpr 비활성화 시 테스트
import pandas as pd
pd.set_option("compute.use_numexpr", False)
# 다시 True로 복귀 가능
pd.set_option("compute.use_numexpr", True)

💡 TIP: pandas는 기본적으로 numexpr가 설치되어 있으면 자동 활성화됩니다.
만약 속도 차이가 느껴지지 않는다면 pd.get_option(“compute.use_numexpr”)로 활성 상태를 확인하세요.

📈 query와 eval의 활용 예시

query는 문자열 기반으로 필터링 조건을 작성할 수 있는 함수이며, 복잡한 논리식을 간결하게 표현할 수 있습니다.
eval은 새로운 열을 계산하거나, 여러 연산을 한 줄로 수행할 때 유용합니다.
두 함수 모두 내부적으로 numexpr를 사용하여 연산을 병렬로 처리합니다.

CODE BLOCK
# query 예시
high_value = df.query("a > 0.8 and b < 0.2")

# eval 예시
df.eval("score = a * 0.5 + b * 0.3 + c * 0.2", inplace=True)

💎 핵심 포인트:
query와 eval은 numexpr 엔진을 통해 병렬 연산과 메모리 최적화를 제공합니다.
대용량 수치 연산에서는 일반 연산보다 훨씬 빠르며, 코드 가독성도 향상됩니다.

⚠️ 주의: numexpr는 문자열 연산이나 object 타입에는 적용되지 않습니다.
또한, eval을 사용할 때 외부 변수 접근이 제한되므로 로컬 변수는 @ 접두사로 참조해야 합니다.



🗂️ 메모리와 자료형 최적화 체크리스트

pandas 성능을 높이는 마지막 단계는 메모리 최적화입니다.
같은 연산이라도 자료형(dtype)이 비효율적이면 메모리 점유가 커지고, 연산 속도도 느려집니다.
특히 대규모 데이터에서는 dtype 관리가 전체 성능을 좌우하기 때문에 반드시 점검해야 합니다.

pandas는 기본적으로 정수형은 int64, 실수형은 float64로 생성되지만, 데이터의 실제 범위에 따라 더 작은 타입으로 변환할 수 있습니다.
또한 문자열 열은 category 타입으로 변환하면 중복 값 저장을 줄여 메모리 사용량을 50% 이상 절감할 수 있습니다.
단, category 타입은 새로운 범주가 생기면 다시 변환이 필요하므로, 고정된 값이 반복되는 컬럼에 적합합니다.

  • 🧩정수형은 int8, int16, int32 등으로 범위에 맞게 축소한다.
  • 💧실수형은 float32로 변환하면 메모리 절반 절약 가능.
  • 🏷️문자열은 category 타입으로 변경해 중복 데이터 압축.
  • 📦datetime 컬럼은 반드시 datetime64로 변환해 연산 최적화.
CODE BLOCK
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "id": np.arange(1_000_000),
    "value": np.random.rand(1_000_000) * 100,
    "category": np.random.choice(["A", "B", "C"], 1_000_000)
})

# 기본 dtype 확인
print(df.dtypes)

# 메모리 절감형 변환
df["id"] = df["id"].astype("int32")
df["value"] = df["value"].astype("float32")
df["category"] = df["category"].astype("category")

print(df.dtypes)
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")

💡 TIP: df.info(memory_usage=’deep’) 명령으로 실제 메모리 점유를 확인할 수 있습니다.
자료형을 변경하기 전후의 차이를 비교하면 최적화 효과를 직관적으로 파악할 수 있습니다.

📊 dtype 최적화 전후 비교

항목 변경 전 변경 후
정수형 int64 int32
실수형 float64 float32
문자열 object category

💎 핵심 포인트:
dtype 최적화는 단순한 정리 작업이 아니라, pandas의 계산 성능을 근본적으로 높이는 핵심 단계입니다.
정확한 타입 관리가 이루어져야 numexpr, 벡터화, 브로드캐스팅이 모두 효율적으로 작동합니다.

자주 묻는 질문 (FAQ)

pandas에서 apply를 써도 괜찮은 경우가 있나요?
네. 벡터화로 표현하기 어려운 복잡한 문자열 처리나 외부 함수 호출이 필요한 경우에만 제한적으로 사용합니다.
그러나 대부분의 수치 연산과 조건 처리에서는 apply 대신 벡터화 표현이 훨씬 효율적입니다.
iterrows와 itertuples의 차이점은 무엇인가요?
iterrows는 각 행을 Series로 반환해 타입이 불안정하고 느리며, itertuples는 NamedTuple로 반환해 속도가 빠릅니다.
특히 대용량 데이터에서는 itertuples가 훨씬 효율적입니다.
numexpr는 모든 pandas 연산에 적용되나요?
아닙니다. numexpr는 수치 연산과 논리식 평가에만 적용됩니다.
문자열 연산, 날짜 처리, object 타입 열에는 적용되지 않습니다.
query와 eval의 차이는 무엇인가요?
query는 행 필터링 조건을 문자열로 작성할 때 사용하며, eval은 계산식이나 새로운 열 생성 등 일반 연산에 사용합니다.
둘 다 numexpr 엔진을 통해 가속됩니다.
dtype 변경이 성능에 영향을 주는 이유는?
더 작은 자료형을 사용하면 메모리 접근이 빨라지고 CPU 캐시 효율이 향상됩니다.
또한 pandas 내부 연산에서 불필요한 형 변환을 방지해 연산 속도가 빨라집니다.
numexpr를 설치하지 않아도 query와 eval을 쓸 수 있나요?
가능합니다. pandas는 numexpr가 없을 때 파이썬의 eval로 대체 실행합니다.
하지만 성능은 다소 낮아집니다.
가능하면 numexpr를 설치해두는 것이 좋습니다.
브로드캐스팅과 벡터화는 같은 개념인가요?
유사하지만 다릅니다.
벡터화는 배열 전체에 대한 연산 최적화를 의미하고, 브로드캐스팅은 크기가 다른 배열 간의 자동 확장 규칙을 말합니다.
둘은 함께 작동해 pandas 연산을 빠르게 만듭니다.
apply 대신 사용할 수 있는 다른 pandas 함수는?
map, assign, transform, pipe 등이 있습니다.
이들은 벡터화와 함께 동작하도록 설계되어 성능과 가독성을 모두 확보할 수 있습니다.

📘 pandas 벡터화와 numexpr로 완성하는 데이터 처리 속도 혁신

pandas 최적화의 핵심은 결국 “파이썬 레벨의 반복문을 제거하고, 내부 C 엔진을 최대한 활용하는 것”에 있습니다.
apply나 iterrows 같은 편의 함수는 가독성은 좋지만, 성능 면에서는 병목의 원인이 되기 쉽습니다.
이를 벡터화, 브로드캐스팅, numexpr 기반의 query·eval로 바꾸면 실행 시간이 수십 배 단축될 수 있습니다.
또한 dtype과 메모리 구조를 최적화하면, 더 큰 데이터셋을 안정적으로 처리할 수 있습니다.

결국 pandas 최적화는 단순한 코드 미세 조정이 아니라, 효율적인 사고방식의 전환입니다.
루프 대신 수학적 연산을, 반복 대신 벡터화된 표현을 선택하는 습관이 중요합니다.
이 원칙을 익히면 pandas뿐 아니라 NumPy, Polars, PySpark 같은 다른 데이터 엔진에서도 동일한 성능 개선 효과를 체감할 수 있습니다.


🏷️ 관련 태그 : pandas성능최적화, 파이썬벡터화, 데이터프레임가속, numexpr활용, apply지양, itertuples대안, queryeval, dtype최적화, 메모리절감, 데이터분석팁