- Published on
pandas read_csv DtypeWarning·메모리 폭발 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 대용량 CSV를 pandas.read_csv로 읽다가 DtypeWarning: Columns ... have mixed types가 뜨고, 이어서 RAM이 급격히 증가해 프로세스가 죽는 경험은 흔합니다. 특히 로그성 데이터, 엑셀에서 내려받은 CSV, 여러 시스템이 합쳐진 덤프 파일은 같은 컬럼에 숫자와 문자열이 섞이거나, 결측 표현이 제각각이라 파서가 dtype 추론을 반복하며 메모리를 과소/과대 할당합니다.
이 글에서는 DtypeWarning이 왜 발생하는지, 메모리 폭발이 어떤 메커니즘으로 일어나는지, 그리고 운영 환경에서도 재현 가능하게 막는 방법(명시적 dtype, 청크 처리, 필요한 컬럼만 읽기, 문자열/범주형 최적화, 파서 옵션)을 코드와 함께 정리합니다. 대용량 처리 중 OOM으로 컨테이너가 죽는 상황은 쿠버네티스의 OOMKilled로도 이어질 수 있으니, 운영 관점의 메모리 관리 감각도 함께 가져가면 좋습니다. 관련해서는 K8s CrashLoopBackOff·OOMKilled 원인과 해결도 같이 참고하면 연결이 잘 됩니다.
DtypeWarning의 정체: “혼합 타입”이 아니라 “추론이 흔들림”
DtypeWarning은 보통 아래 상황에서 발생합니다.
- 같은 컬럼에
1,2,3같은 숫자와N/A,-,unknown, 공백 같은 문자열이 섞임 - 초반 몇만 행에서는 숫자로만 보였는데 뒤쪽에서 문자열이 등장
- 일부 행에서
1e5같은 과학 표기, 혹은00123같은 선행 0이 등장 - 큰 파일을
low_memory=True(기본값)로 읽어 청크 단위로 dtype 추론을 하다가, 청크 간 dtype이 달라 경고 발생
핵심은 “pandas가 한 번에 전체를 보고 dtype을 결정하지 못했다”는 점입니다. 경고 자체는 치명적이지 않을 수 있지만, 이 상태가 메모리 폭발의 전조가 되는 경우가 많습니다.
메모리 폭발이 일어나는 대표 메커니즘
1) 숫자 컬럼이 object로 승격되면 메모리가 급증
숫자 컬럼이 int64나 float64로 들어오면 배열이 연속 메모리(NumPy)로 잡혀 효율적입니다. 하지만 문자열이 섞여 object가 되면, 각 셀이 파이썬 객체 포인터로 바뀌어 메모리 오버헤드가 크게 증가합니다.
int64: 값 8바이트 중심object: 포인터 + 파이썬 객체 오버헤드 + 문자열 저장 공간
2) dtype 추론 과정에서 임시 버퍼가 커짐
read_csv는 파싱 과정에서 내부 버퍼를 만들고, dtype을 바꾸는 변환을 수행합니다. 혼합 타입 컬럼이 많으면 변환과 재할당이 반복되며 피크 메모리가 커집니다.
3) low_memory=True는 메모리 절약 옵션이 아니라 “추론을 쪼개는 옵션”
이 옵션은 내부적으로 파일을 청크로 나눠 dtype을 추론합니다. 결과적으로 dtype이 흔들리면 DtypeWarning이 뜨고, 최종적으로 object로 수렴하면서 메모리 사용량이 늘 수 있습니다.
해결 전략 1: dtype을 명시해서 “추론”을 없애기
가장 확실한 방법은 dtype을 직접 지정하는 것입니다. 특히 식별자(id), 코드값, 우편번호, 전화번호 등은 숫자처럼 보여도 문자열이어야 합니다.
import pandas as pd
dtype_map = {
"user_id": "string",
"order_id": "string",
"zip": "string",
"amount": "float64",
"status": "category",
}
df = pd.read_csv(
"data.csv",
dtype=dtype_map,
encoding="utf-8",
)
string vs object
가능하면 object 대신 string(pandas StringDtype)을 사용하세요.
- 결측이
pd.NA로 정규화되어 처리 일관성이 좋아짐 - 이후
.str연산이 더 예측 가능
정수 컬럼에 결측이 있으면 Int64(대문자 I)
CSV에 결측이 섞인 정수는 int64로 못 들어갑니다. 이때는 nullable integer를 씁니다.
import pandas as pd
df = pd.read_csv(
"data.csv",
dtype={"qty": "Int64"},
)
해결 전략 2: 필요한 컬럼만 읽기(usecols) + 행 필터링
메모리 폭발의 가장 쉬운 예방은 “안 읽는 것”입니다.
import pandas as pd
usecols = ["user_id", "event_time", "event_type", "amount"]
df = pd.read_csv(
"events.csv",
usecols=usecols,
dtype={
"user_id": "string",
"event_type": "category",
"amount": "float32",
},
parse_dates=["event_time"],
)
float64가 꼭 필요 없다면float32로 절반 절약 가능- 범주형은 값 종류가 제한적일 때 효과가 큼
해결 전략 3: 청크 처리로 피크 메모리 낮추기
파일 전체를 한 번에 DataFrame으로 만들지 말고, chunksize로 스트리밍 처리합니다. 집계/필터/저장 같은 파이프라인에 특히 유리합니다.
import pandas as pd
chunks = pd.read_csv(
"events.csv",
chunksize=200_000,
dtype={
"user_id": "string",
"event_type": "category",
"amount": "float32",
},
parse_dates=["event_time"],
)
result = []
for chunk in chunks:
# 예: 필요한 이벤트만 필터
chunk = chunk[chunk["event_type"].isin(["purchase", "refund"])]
# 예: 월별 합계
chunk["month"] = chunk["event_time"].dt.to_period("M").astype("string")
agg = chunk.groupby("month", as_index=False)["amount"].sum()
result.append(agg)
final = pd.concat(result, ignore_index=True)
final = final.groupby("month", as_index=False)["amount"].sum()
청크 처리의 포인트는 “중간 결과를 작게 유지”하는 것입니다.
- 청크별로 필터링/집계해서 버리고
- 최종적으로 필요한 결과만
concat
해결 전략 4: 결측/이상치 문자열을 미리 NA로 통일하기
혼합 타입의 원인이 N/A, null, - 같은 값이라면, na_values로 강제 결측 처리하면 dtype이 안정됩니다.
import pandas as pd
na_tokens = ["N/A", "NA", "NULL", "null", "-", "", " "]
df = pd.read_csv(
"data.csv",
na_values=na_tokens,
keep_default_na=True,
dtype={
"amount": "float64",
"user_id": "string",
},
)
여기서 중요한 점은 na_values를 넣어도 dtype 추론이 완전히 안정되는 건 아니라는 것입니다. 가능하면 dtype과 같이 사용하세요.
해결 전략 5: 파서 옵션으로 불필요한 비용 줄이기
low_memory=False로 경고를 “숨기는” 게 아니라 “일관 추론” 유도
low_memory=False는 한 번에 더 많은 메모리를 쓰는 대신, 더 일관된 dtype 추론을 하게 만들어 DtypeWarning을 줄일 수 있습니다.
import pandas as pd
df = pd.read_csv(
"data.csv",
low_memory=False,
)
하지만 이건 근본 해결이 아니라 “추론을 더 크게 한 번에” 하는 방식이라, 파일이 매우 크면 오히려 위험할 수 있습니다. 대용량에서는 dtype 명시 또는 chunksize가 우선입니다.
engine="pyarrow" 고려(가능한 환경에서)
환경이 허용한다면 pyarrow 엔진은 메모리 효율과 속도에서 이점이 있을 수 있습니다.
import pandas as pd
# pandas 버전에 따라 동작/지원 범위가 다를 수 있음
df = pd.read_csv(
"data.csv",
engine="pyarrow",
dtype_backend="pyarrow",
)
dtype_backend를 pyarrow로 두면 문자열/정수의 메모리 효율이 좋아질 수 있습니다. 다만 모든 옵션 조합이 동일하게 동작하지 않을 수 있으니, 운영 투입 전 샘플 파일로 검증하세요.
해결 전략 6: 읽은 직후 메모리 다이어트(다운캐스트·범주형)
이미 읽어버린 뒤라면, 컬럼별로 다운캐스트를 적용해 메모리를 줄일 수 있습니다.
import pandas as pd
# 숫자 다운캐스트
for col in ["amount", "score"]:
df[col] = pd.to_numeric(df[col], errors="coerce", downcast="float")
for col in ["qty", "age"]:
df[col] = pd.to_numeric(df[col], errors="coerce", downcast="integer")
# 범주형 전환(카디널리티가 낮을 때)
for col in ["status", "country"]:
df[col] = df[col].astype("category")
다운캐스트는 “정밀도 손실” 가능성이 있으니, 금액처럼 정확성이 중요한 값은 float 대신 정수(예: 센트 단위)로 저장하는 설계를 검토하는 것도 방법입니다.
실전 디버깅: 어떤 컬럼이 문제인지 빠르게 찾기
1) 경고에서 찍힌 컬럼을 샘플링
문제 컬럼이 object로 들어왔는지 확인합니다.
problem_cols = [c for c in df.columns if df[c].dtype == "object"]
problem_cols[:20]
2) 숫자여야 하는데 문자열이 섞였는지 검사
import pandas as pd
col = "amount"
parsed = pd.to_numeric(df[col], errors="coerce")
ratio_bad = parsed.isna().mean()
print("bad_ratio:", ratio_bad)
# 원본에서 숫자 파싱 실패한 값 일부 보기
bad_values = df.loc[parsed.isna(), col].astype("string").value_counts().head(20)
print(bad_values)
이 결과로 ,가 포함된 "1,234", 통화 기호 "$10", 공백, "-" 같은 토큰을 확인하고 전처리 전략을 결정합니다.
운영 팁: CSV를 계속 쓸 건가?
대용량 CSV는 파싱 비용과 dtype 불안정성 때문에 ETL 파이프라인에서 병목이 되기 쉽습니다. 반복 처리라면 다음을 고려하세요.
- 1회 변환 후 Parquet로 저장
- 다음부터는
read_parquet로 로딩
import pandas as pd
df = pd.read_csv(
"events.csv",
dtype={"user_id": "string", "event_type": "category"},
parse_dates=["event_time"],
)
df.to_parquet("events.parquet", index=False)
# 이후
fast = pd.read_parquet("events.parquet")
이렇게 하면 dtype이 고정되고, IO도 줄어들며, 메모리 사용량이 예측 가능해집니다.
체크리스트: DtypeWarning과 메모리 폭발을 동시에 막는 최소 세트
usecols로 필요한 컬럼만 읽기dtype를 명시(특히 id/코드/우편번호는string)- 결측 토큰은
na_values로 통일 - 파일이 크면
chunksize로 스트리밍 처리 - 값 종류가 적은 문자열은
category로 전환 - 반복 작업이면 CSV를 Parquet로 변환
대용량 데이터 처리에서 메모리 문제는 애플리케이션 코드만의 문제가 아니라, 실행 환경에서의 OOM과 재시작으로 이어지는 운영 이슈가 됩니다. 컨테이너에서 갑자기 죽는다면 K8s CrashLoopBackOff·OOMKilled 원인과 해결 관점으로도 같이 점검해보면 원인 파악이 빨라집니다.