- Published on
Pandas read_csv DtypeWarning·dtype 혼합 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 로그, 결제 내역, CRM 덤프처럼 현실의 CSV는 깔끔한 스키마를 보장하지 않습니다. 같은 컬럼에 123, 00123, N/A, -, 1.2e3, 공백이 뒤섞이면 Pandas는 타입을 추론하다가 중간에 결정을 바꾸게 되고, 그 결과로 DtypeWarning: Columns ... have mixed types 경고가 뜹니다.
이 경고를 low_memory=False로 “조용히” 만드는 방법도 있지만, 근본적으로는 어떤 값이 섞였는지를 찾고 어떤 타입으로 수렴시킬지를 결정해야 이후의 집계, 조인, 저장(Parquet/DB 적재)에서 사고가 나지 않습니다. 아래는 실무에서 재현 가능한 패턴과 해결책을 단계별로 정리한 글입니다.
DtypeWarning이 발생하는 진짜 이유
Pandas의 read_csv는 기본적으로 컬럼 타입을 자동 추론합니다. 문제는 파일이 크면 메모리를 아끼기 위해 내부적으로 여러 블록(청크 비슷한 단위)로 읽고, 각 블록에서 관측된 값으로 dtype을 추론한 뒤 합치는 과정에서 충돌이 나면 경고를 띄운다는 점입니다.
대표적인 혼합 사례는 다음과 같습니다.
- 숫자 컬럼에 문자열 토큰이 섞임:
"-","N/A","NULL","" - 정수처럼 보이지만 앞자리 0이 중요한 코드:
"00123" - 천 단위 콤마가 포함된 숫자:
"1,234" - 통화/단위가 붙은 값:
"$12.3","10kg" - 일부 행에만 소수점이 존재:
1과1.5혼재 - 매우 큰 정수(식별자)가 float로 승격되며 정밀도 손실
핵심은 “경고를 없애는 것”이 아니라 스키마를 정하고 그 스키마로 강제 변환하는 것입니다.
재현 예시: 작은 CSV로 문제 만들기
아래 예시는 동일 컬럼에 숫자와 문자가 섞여 dtype이 흔들리는 전형적인 케이스입니다.
import pandas as pd
from io import StringIO
csv_text = """order_id,amount,user_code
1,100,00123
2,200,00456
3,-,00789
4,150.5,01000
"""
df = pd.read_csv(StringIO(csv_text))
print(df.dtypes)
print(df)
amount는 숫자여야 하지만 - 때문에 문자열로 섞일 수 있고, user_code는 숫자처럼 보여도 앞자리 0이 의미가 있으므로 문자열로 유지해야 안전합니다.
1) 가장 안전한 해법: dtype을 명시적으로 지정
가장 권장되는 방식은 dtype으로 컬럼 스키마를 고정하는 것입니다. 단, 숫자 컬럼에 비정상 토큰이 섞여 있으면 바로 파싱 에러가 날 수 있으니 na_values와 함께 쓰는 것이 실전적입니다.
import pandas as pd
na_tokens = ["-", "N/A", "NA", "NULL", "null", "", " "]
dtype_map = {
"order_id": "Int64", # 결측 허용 정수
"user_code": "string", # 앞자리 0 보존
}
df = pd.read_csv(
"orders.csv",
dtype=dtype_map,
na_values=na_tokens,
keep_default_na=True,
)
# amount는 우선 string으로 읽고, 후처리로 숫자 변환하는 전략도 안전함
여기서 포인트는 다음과 같습니다.
Int64는 Pandas의 nullable integer dtype입니다. 결측치가 섞여도int로 유지할 수 있습니다.user_code같은 식별자는 숫자로 읽으면 앞자리 0이 사라질 수 있으니string이 안전합니다.- 숫자 컬럼이 지저분하면 “처음부터 float로 강제”하기보다 문자열로 읽고 정제 후 변환이 디버깅에 유리합니다.
2) converters로 컬럼별 전처리를 읽는 순간에 적용
dtype은 “파서가 그대로 해석한 결과”를 어떤 dtype으로 담을지 결정합니다. 반면 converters는 문자열 원본을 함수로 가공해서 넣을 수 있어, 콤마 제거/통화 기호 제거 같은 정제에 강합니다.
import pandas as pd
def parse_amount(x: str):
if x is None:
return pd.NA
x = str(x).strip()
if x in {"-", "", "N/A", "NA", "NULL", "null"}:
return pd.NA
x = x.replace(",", "")
x = x.replace("$", "")
return x
df = pd.read_csv(
"orders.csv",
dtype={"user_code": "string"},
converters={"amount": parse_amount},
)
# 정제된 문자열을 숫자로 변환
# errors는 반드시 'coerce'로 두고, 변환 실패는 결측으로 만든 뒤 검증하는 편이 안전
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
이 접근은 “문제 데이터를 숨기지 않고” 추적 가능하게 만듭니다. 변환 실패가 NaN으로 들어오면, 이후에 어떤 행이 실패했는지 쉽게 검사할 수 있습니다.
3) low_memory=False는 해결책이 아니라 진단 보조
read_csv(..., low_memory=False)는 더 많은 데이터를 한 번에 보고 dtype을 결정하게 만들어 경고를 줄일 수 있습니다. 하지만 혼합 타입 자체가 사라지는 것은 아닙니다.
- 장점: 경고가 줄고 dtype 추론이 덜 흔들림
- 단점: 메모리 사용량 증가, 그리고 “잘못된 추론”이 조용히 발생할 수 있음
즉, 다음처럼 쓰는 것은 괜찮지만, 최종 해법은 dtype 또는 정제 파이프라인이어야 합니다.
import pandas as pd
df = pd.read_csv("big.csv", low_memory=False)
4) 대용량 CSV: 청크로 읽고 스키마를 확정하는 패턴
파일이 크고 컬럼 품질이 불명확하면, 먼저 일부를 샘플링해 “혼합 원인”을 잡고 스키마를 확정한 다음 전체를 읽는 방식이 효율적입니다.
import pandas as pd
sample = pd.read_csv("big.csv", nrows=20000)
# 의심 컬럼에서 숫자 변환 실패 행을 찾아보기
col = "amount"
parsed = pd.to_numeric(sample[col].astype("string"), errors="coerce")
bad = sample.loc[parsed.isna() & sample[col].notna(), [col]]
print(bad.head(20))
print("bad rows:", len(bad))
그 다음, 확정한 규칙으로 전체를 청크 처리합니다.
import pandas as pd
na_tokens = ["-", "N/A", "NA", "NULL", "null", "", " "]
chunks = []
for chunk in pd.read_csv(
"big.csv",
chunksize=200_000,
dtype={"user_code": "string"},
na_values=na_tokens,
):
chunk["amount"] = (
chunk["amount"]
.astype("string")
.str.replace(",", "", regex=False)
)
chunk["amount"] = pd.to_numeric(chunk["amount"], errors="coerce")
chunks.append(chunk)
df = pd.concat(chunks, ignore_index=True)
이 패턴은 메모리 제약이 있는 환경(EKS 배치 잡, CI, 노트북)에서 특히 유용합니다. 대용량 처리·자원 이슈는 본질적으로 운영 문제이기도 하니, 비슷한 맥락의 트러블슈팅 관점은 EKS Pod Pending(Insufficient cpu) 원인과 해결 같은 글도 같이 참고하면 좋습니다.
5) “숫자처럼 보이는 식별자”는 문자열로 고정
DtypeWarning과 별개로, 실무에서 더 위험한 함정은 식별자 컬럼을 숫자로 읽어버리는 것입니다.
- 주문번호, 사용자코드, 우편번호, SKU 등은 연산 대상이 아니라 “라벨”
- 앞자리 0이 의미가 있거나, 길이가 고정된 경우가 많음
- 정수 범위를 넘어가는 값은 float로 승격되며 정밀도 손실 가능
권장 스키마:
dtype_map = {
"order_id": "string",
"user_id": "string",
"zip": "string",
"sku": "string",
}
df = pd.read_csv("data.csv", dtype=dtype_map)
필요하면 후처리로 포맷 검증(길이, 정규식)을 추가하세요.
6) 날짜/시간 컬럼은 parse_dates보다 명시적 변환이 디버깅에 유리한 경우가 많음
parse_dates는 편하지만, 입력이 지저분하면 어떤 값이 실패했는지 파악이 어려울 수 있습니다. 특히 타임존 표기나 잘못된 날짜가 섞이면 “조용히 NaT”가 늘어날 수 있습니다.
import pandas as pd
s = pd.read_csv("events.csv", dtype={"event_time": "string"})
s["event_time"] = pd.to_datetime(
s["event_time"],
errors="coerce",
utc=True,
)
# 실패한 원본을 확인
bad = s.loc[s["event_time"].isna(), ["event_time"]]
print(bad.head())
7) 경고를 “숨기기” 전에 품질 게이트를 만들기
DtypeWarning을 단순히 없애는 것보다 중요한 건, 데이터 파이프라인에서 혼합 타입을 조기에 감지하고 실패/격리하는 체계를 만드는 것입니다.
간단한 품질 게이트 예시:
import pandas as pd
def assert_numeric_rate(df: pd.DataFrame, col: str, min_rate: float = 0.99):
x = pd.to_numeric(df[col].astype("string"), errors="coerce")
rate = x.notna().mean()
if rate < min_rate:
raise ValueError(f"{col} numeric rate too low: {rate:.4f}")
df = pd.read_csv("orders.csv", dtype={"amount": "string"})
assert_numeric_rate(df, "amount", min_rate=0.98)
이런 방식은 배치 적재에서 특히 효과적입니다. 경고를 무시한 채 진행하면, 나중에 집계가 틀어지거나 DB 타입 변환에서 실패하며 더 큰 장애로 번질 수 있습니다. 운영 관점의 “조기 진단”은 데이터베이스에서도 동일하게 중요하니, 예를 들어 PostgreSQL VACUUM 안됨? autovacuum 튜닝 실전처럼 사소한 경고/징후를 초기에 잡는 사고방식이 도움이 됩니다.
실무에서 자주 쓰는 조합 레시피
상황별로 가장 많이 쓰는 조합을 요약하면 다음과 같습니다.
레시피 A: 파일이 작고 스키마가 명확
dtype로 고정na_values로 결측 토큰 통일
df = pd.read_csv(
"data.csv",
dtype={"id": "string", "qty": "Int64"},
na_values=["-", "N/A", "NULL"],
)
레시피 B: 숫자 컬럼이 지저분(콤마/통화/단위)
- 해당 컬럼만
string으로 읽기 - 정제 후
to_numeric(errors="coerce")
df = pd.read_csv("data.csv", dtype={"amount": "string"})
df["amount"] = df["amount"].str.replace(",", "", regex=False)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
레시피 C: 대용량 + 품질 불명확
- 샘플로 이상값 프로파일링
chunksize로 처리- 변환 실패율 게이트
for chunk in pd.read_csv("big.csv", chunksize=100_000, dtype={"amount": "string"}):
chunk["amount"] = pd.to_numeric(chunk["amount"], errors="coerce")
if chunk["amount"].notna().mean() < 0.95:
raise ValueError("too many invalid amounts")
마무리: DtypeWarning은 “데이터 계약이 깨졌다”는 신호
DtypeWarning은 Pandas의 귀찮은 잔소리가 아니라, CSV가 이미 스키마를 위반하고 있다는 신호입니다. 해결의 우선순위는 다음 순서가 가장 안전합니다.
- 문제 컬럼을
string으로 읽어 원본을 보존 na_values와 정제 규칙으로 입력을 표준화to_numeric또는to_datetime로 명시 변환하고 실패를 계측- 최종 스키마를
dtype로 고정해 재현성 확보
이 과정을 갖추면 경고를 없애는 수준을 넘어, 데이터 적재/분석 파이프라인의 신뢰도가 크게 올라갑니다. 대용량 처리에서 리소스가 빡빡해질 때는 운영 환경 튜닝도 같이 고려해야 하며, 쿠버네티스 환경이라면 Kubernetes CrashLoopBackOff와 OOMKilled(ExitCode 137) 해결처럼 메모리/CPU 관점의 점검도 병행하는 것이 좋습니다.