pandas 에서 PySpark 로 — 단일 머신 한계를 넘는 스케일아웃
메모리에 안 들어가는 데이터를 만난 pandas 사용자를 위한 전환 가이드. lazy vs eager 실행 모델 차이, 흔히 빠지는 함정(반복문·인덱스·collect), pandas API on Spark 로 코드 거의 그대로 분산 처리하는 법까지 정리합니다.
pandas 로 잘 돌던 분석 코드가 데이터가 커지면서 MemoryError 를 내기 시작합니다. 단일 머신 메모리에 데이터가 안 들어가는 순간, PySpark 로의 전환을 고민하게 됩니다. 그런데 pandas 와 Spark 는 실행 모델이 근본적으로 달라서, 익숙한 pandas 습관을 그대로 옮기면 느려지거나 오히려 메모리가 터집니다.
이 글은 pandas 사용자가 PySpark 로 넘어올 때 반드시 이해해야 할 차이, 흔히 빠지는 함정, 그리고 코드를 거의 바꾸지 않고 분산 처리하는 길(pandas API on Spark)을 정리합니다.
1. 가장 큰 차이 — Lazy vs Eager
pandas 는 즉시 실행(eager) 합니다. 한 줄 쓰면 그 자리에서 계산하고 결과를 메모리에 들고 있습니다. Spark 는 지연 실행(lazy) 합니다. 변환(transformation)을 쌓아두기만 하다가, 액션(action)을 만나야 한 번에 최적화해서 실행합니다.
# pandas: 각 줄이 즉시 실행
df2 = df[df.amount > 100] # 바로 필터링됨
df3 = df2.groupby("user").sum() # 바로 집계됨
# PySpark: 변환은 계획만, 액션에서 실행
df2 = df.filter(F.col("amount") > 100) # 아직 실행 안 됨
df3 = df2.groupBy("user").sum() # 여전히 계획만
df3.show() # ← 여기서 비로소 전체 실행| pandas (eager) | PySpark (lazy) | |
|---|---|---|
| 실행 시점 | 줄마다 즉시 | 액션에서 한 번에 |
| 최적화 | 없음 | Catalyst 가 전체 계획 최적화 |
| 디버깅 | 중간 결과 즉시 확인 | 액션 전엔 확인 불가 |
이 차이를 이해하면 "왜 에러가 엉뚱한 줄에서 나는지"(실제로는 액션에서 전체가 실행되며 터짐)를 알 수 있습니다.
2. 함정 ① 반복문 — 절대 행을 순회하지 마라
pandas 의 iterrows, apply 같은 행 순회 습관이 Spark 에서 최악입니다.
# pandas (이미 느리지만 동작)
for idx, row in df.iterrows():
df.at[idx, "grade"] = compute(row)
# PySpark 에서 이걸 흉내내면? → collect 로 드라이버 메모리 폭발 + 분산 무력화
# 올바른 방법: 컬럼 연산(벡터화)
df = df.withColumn("grade",
F.when(F.col("score") >= 90, "A").otherwise("B"))Spark 의 힘은 컬럼 단위 분산 연산입니다. 행을 순회하는 순간 분산의 이점이 사라집니다. pandas 의 apply(axis=1) 도 마찬가지 — 컬럼 표현식이나 (불가피하면) pandas_udf 로 바꿔야 합니다(별도 글 "PySpark UDF가 느린 이유와 Pandas UDF").
3. 함정 ② collect / toPandas — 드라이버로 다 끌어오기
# 위험: 분산 데이터를 단일 드라이버 메모리로 → 큰 데이터면 OOM
result = spark_df.toPandas()
rows = spark_df.collect()
# 안전: 분산 저장하거나, 작게 줄인 뒤에만
spark_df.write.parquet("out") # 익스큐터가 분산 저장
sample = spark_df.limit(1000).toPandas() # 필요한 만큼만
agg = spark_df.groupBy("k").count().toPandas() # 집계 후 작은 결과만pandas 로 "돌아오고" 싶은 충동이 가장 위험합니다. 결과가 크면 절대 드라이버로 모으지 마세요(별도 글 "PySpark Executor OOM 정복"의 드라이버 OOM 참고).
4. 함정 ③ 인덱스 — Spark 에는 행 인덱스가 없다
pandas 의 핵심인 인덱스가 Spark 에는 없습니다. 순서·위치 기반 접근(df.iloc[5], df.loc[idx])이 안 됩니다.
# pandas: 위치/인덱스 접근
df.iloc[0]
df.set_index("id")
# PySpark: 순서가 필요하면 명시적으로 정렬 + window
from pyspark.sql.window import Window
df = df.withColumn("row_num",
F.row_number().over(Window.orderBy("created_at")))Spark 데이터는 분산되어 순서가 없습니다. 순서가 필요하면 명시적으로 정렬하고, 행 번호가 필요하면 window 함수를 씁니다.
5. 함정 ④ 데이터 타입과 NULL
# pandas: NaN, 동적 타입
# PySpark: 명시적 스키마, null
# pandas 의 object 컬럼, mixed type → Spark 는 스키마가 엄격
# CSV 읽을 때 스키마 추론에 의존하지 말고 명시
schema = "id long, amount double, name string"
df = spark.read.schema(schema).csv("path", header=True)6. API 매핑 — 손에 익은 연산들
| 작업 | pandas | PySpark |
|---|---|---|
| 필터 | df[df.x > 1] | df.filter(F.col("x") > 1) |
| 컬럼 추가 | df["y"] = ... | df.withColumn("y", ...) |
| 집계 | df.groupby("k").sum() | df.groupBy("k").sum() |
| 조인 | pd.merge(a, b, on="k") | a.join(b, "k") |
| 정렬 | df.sort_values("x") | df.orderBy("x") |
| 컬럼명 변경 | df.rename(...) | df.withColumnRenamed(...) |
| 결측 처리 | df.fillna(0) | df.fillna(0) / F.coalesce |
| 고유값 | df.x.unique() | df.select("x").distinct() |
| 행 수 | len(df) | df.count() (액션!) |
7. 가장 쉬운 길 — pandas API on Spark
코드를 거의 안 바꾸고 분산 처리하고 싶다면 pandas API on Spark(구 Koalas, pyspark.pandas)를 씁니다. pandas 와 거의 같은 API 를 Spark 위에서 실행합니다.
import pyspark.pandas as ps
# pandas 코드와 거의 동일!
psdf = ps.read_parquet("hdfs://.../big_data")
result = (psdf[psdf.amount > 100]
.groupby("user")["amount"]
.sum()
.sort_values(ascending=False))
# 진짜 pandas 가 필요한 작은 결과만 변환
small = result.head(100).to_pandas()| 순수 PySpark | pandas API on Spark | |
|---|---|---|
| 코드 변경 | 많음(API 다름) | 거의 없음 |
| 친숙함 | 새 API 학습 | pandas 그대로 |
| 세밀 제어 | 높음 | 다소 추상화됨 |
| 적합 | 신규/성능 critical | pandas 코드 이식 |
전환 전략: 기존 pandas 코드가 많다면 pandas API on Spark 로 먼저 이식해 동작시키고, 성능이 중요한 핫스팟만 순수 PySpark 로 다시 쓰는 점진적 접근이 현실적입니다.
8. 작은 데이터엔 Spark 가 과하다
반대 방향의 함정도 있습니다. 데이터가 작으면 Spark 가 오히려 느립니다. 셔플·JVM·스케줄링 오버헤드 때문입니다.
| 데이터 규모 | 권장 |
|---|---|
| 수 GB 이하, 단일 머신 가능 | pandas (또는 Polars/DuckDB) |
| 단일 머신 메모리 초과 | PySpark |
| pandas 코드 많은데 커짐 | pandas API on Spark |
"큰 데이터 = Spark"가 항상 옳은 건 아닙니다. 단일 머신에 들어가면 pandas/Polars/DuckDB 가 더 빠르고 간단합니다.
9. 전환 체크리스트
- lazy 실행 이해 — 액션 전엔 실행 안 됨
-
iterrows/apply(axis=1)→ 컬럼 연산/pandas_udf -
toPandas/collect금지 — 분산 저장 또는 작게 - 인덱스 의존 제거 → 정렬 + window
- 스키마 명시 (추론 의존 줄이기)
- 큰 pandas 코드는 pandas API on Spark 로 이식
- 작은 데이터는 굳이 Spark 안 쓰기
10. 정리
| 차이/함정 | pandas 습관 | PySpark 정답 |
|---|---|---|
| 실행 | eager | lazy(액션에서 실행) |
| 행 처리 | iterrows/apply | 컬럼 벡터 연산 |
| 결과 수집 | 다 메모리 | 분산 저장/limit |
| 순서 | 인덱스 | 정렬 + window |
| 이식 | — | pandas API on Spark |
pandas 에서 PySpark 로의 전환은 "API 를 바꾸는 일"이 아니라 "사고방식을 바꾸는 일" 입니다. 즉시 실행·행 순회·전체 메모리 보유라는 pandas 의 전제가 분산 환경에서는 모두 반대가 됩니다. lazy 실행과 컬럼 연산을 이해하고, toPandas 의 유혹을 참으며, 기존 코드는 pandas API on Spark 로 점진적으로 옮기는 것 — 이 길이 단일 머신의 한계를 가장 매끄럽게 넘는 방법입니다.
이 글은 Spark 3.5 기준으로 작성되었습니다. pandas 기반 분석의 스케일아웃이나 데이터 파이프라인 전환이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀