목차
1~11장을 한 번 본 뒤 다시 읽는 챕터입니다. pandas에서 반복적으로 사람을 잡는 것들: ① apply/iterrows 남용, ② 연쇄 인덱싱과 SettingWithCopyWarning, ③ dtype·메모리 낭비, ④ inplace 오해, ⑤ 자동 정렬로 생기는 결측. 마지막에 method chaining과 pandas를 벗어날 시점을 정리합니다.
import pandas as pd
import numpy as npapply > iterrows속도 순서는 대체로 벡터 연산 ≫ .str/.dt ≫ apply ≫ itertuples ≫ iterrows 입니다.
# 최악: iterrows (행마다 Series 생성)
totals = []
for _, row in df.iterrows():
totals.append(row["price"] * row["qty"])
# 나쁨: apply(axis=1)
df["total"] = df.apply(lambda r: r["price"] * r["qty"], axis=1)
# 좋음: 벡터 연산
df["total"] = df["price"] * df["qty"]루프를 못 없앨 때의 대안:
np.where, np.select, .mask()/.where().map(dict), .replace(dict)groupby().agg/transform (Python - pandas 08. 그룹 연산 (groupby))itertuples() (iterrows보다 훨씬 빠름), Numba, Cython💡 이럴 때:
apply/iterrows가 보이면 벡터 연산·접근자·groupby로 바꿀 수 있는지부터 확인합니다. 큰 데이터에서 10~100배 차이.
# 문제
df[df["age"] > 30]["bonus"] = 100 # copy에 쓸 수 있음 → 원본 미반영 + 경고
# 해결: .loc 한 번에
df.loc[df["age"] > 30, "bonus"] = 100부분집합을 따로 오래 쓸 거면 .copy():
sub = df.loc[df["region"] == "KR"].copy()
sub["x"] = sub["x"] * 2 # 원본 df 영향 없음df[...]가 view인지 copy인지 pandas가 보장하지 못함.df.loc[...] = ..." 규칙은 그대로입니다. 미리 pd.set_option("mode.copy_on_write", True)로 켜서 테스트할 수 있습니다.df.memory_usage(deep=True).sum() / 1e6
# 범주형 전환
for c in df.select_dtypes("object"):
if df[c].nunique() / len(df) < 0.5:
df[c] = df[c].astype("category")
# 수치 다운캐스트
df["qty"] = pd.to_numeric(df["qty"], downcast="integer") # int64 → int8/16/32
df["ratio"] = pd.to_numeric(df["ratio"], downcast="float") # float64 → float32
# 문자열: pyarrow 백엔드
df["name"] = df["name"].astype("string[pyarrow]")💡 이럴 때: 데이터가 메모리에 겨우 들어가거나 groupby가 느릴 때. 로드 직후 한 번 최적화하면 이후 모든 연산이 가벼워집니다.
inplace 논쟁df.dropna(inplace=True) # 권장하지 않음
df = df.dropna() # 권장 (재대입)inplace=True가 메모리를 아낀다는 것은 대체로 오해입니다. 내부적으로 복사가 일어나는 경우가 많고, method chaining을 끊으며, CoW 도입으로 의미가 더 모호해졌습니다.a = pd.Series([1, 2, 3], index=["x", "y", "z"])
b = pd.Series([10, 20], index=["y", "z"])
a + b # x=NaN, y=12, z=23 ← 라벨 불일치 → 조용히 NaN
a.add(b, fill_value=0) # x=1, y=12, z=23reindex, shift, merge, concat(axis=1) 모두 라벨 정렬로 NaN을 만들 수 있습니다. 연산 후 isna().sum()을 습관적으로 확인합니다 → Python - pandas 05. 결측치 처리.
.pipe()result = (
df
.query("amount > 0")
.assign(month=lambda d: d["order_date"].dt.to_period("M"))
.groupby(["month", "city"], observed=True, as_index=False)
.agg(revenue=("amount", "sum"))
.sort_values("revenue", ascending=False)
.pipe(add_share) # 사용자 정의 함수를 체인에 끼움
)
def add_share(d):
d["share"] = d["revenue"] / d["revenue"].sum()
return ddf1, df2...)를 없애 "무엇을 하는 파이프라인인지" 한눈에 보입니다..pipe(func)는 func(df)를 호출하되 체인 안에 자연스럽게 들어가게 합니다..pipe(lambda d: (print(d.shape), d)[1])로 중간 상태를 찍을 수 있습니다.💡 이럴 때: 전처리 단계가 5개 이상이면 chaining으로 묶어 재현성과 가독성을 높입니다.
eval / query — 대용량에서 중간 배열 절감df.eval("total = price * qty - discount", inplace=False)
df.query("total > 1000 and city == 'Seoul'")numexpr 엔진이 있으면 큰 DataFrame에서 임시 배열을 덜 만들어 메모리·속도 이득. 작은 데이터에서는 파싱 오버헤드로 오히려 느릴 수 있습니다.
read_csv(chunksize=...)로 나눠 집계.💡 이럴 때: 데이터가 수 GB를 넘고 groupby/join이 분 단위로 걸리면 Polars 또는 DuckDB로 무거운 집계만 넘기고, 결과를 pandas로 받아 마무리하는 하이브리드가 실용적입니다.
apply(axis=1) / iterrows가 남아 있지 않은가?.loc 한 번으로 하고 있는가? (연쇄 인덱싱 제거)object 열 중 범주형으로 바꿀 것이 있는가?int64→int32, float64→float32)merge 후 행 수가 예상대로인가? (validate, indicator)NaN이 생기지 않았는가? (자동 정렬)
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.