Published on

pandas DtypeWarning과 혼합타입 CSV 안전히 읽기

Authors

서버 로그, 결제 내역, 사용자 이벤트처럼 현실의 CSV는 깔끔하지 않습니다. 같은 컬럼에 숫자와 문자열이 섞이거나, 일부 행에만 N/A, 공백, - 같은 값이 들어가면서 pandas는 타입 추론에 실패합니다. 그 결과로 흔히 다음 경고를 보게 됩니다.

DtypeWarning: Columns (x) have mixed types. Specify dtype option on import or set low_memory=False.

이 글은 단순히 low_memory=False로 덮는 방법이 아니라, 왜 경고가 발생하는지, 어떤 컬럼이 문제인지 빠르게 찾는 법, 그리고 다운스트림(집계/조인/DB 적재)까지 안전한 CSV 로딩 패턴을 정리합니다.

DtypeWarning이 발생하는 원리

read_csv는 기본적으로 컬럼 타입을 “추정”합니다. 문제는 파일이 크면 pandas가 메모리를 아끼기 위해 청크 단위로 나눠 읽고, 각 청크에서 관측된 값으로 타입을 추정한다는 점입니다.

  • 앞부분 청크에서는 숫자로만 보여서 int64로 추정
  • 뒷부분 청크에서 "00123", "N/A", "-" 같은 문자열이 등장
  • 청크 간 타입이 충돌하면서 “혼합 타입” 경고가 발생

즉, 경고는 단순한 소음이 아니라 타입이 비결정적(Non-deterministic) 이라는 신호입니다. 같은 파일이라도 읽는 방식/버전에 따라 타입이 달라질 수 있고, 이는 분석 결과를 흔들거나 ETL 파이프라인을 깨뜨립니다.

가장 흔한 혼합 타입 패턴

현업에서 자주 터지는 케이스는 다음과 같습니다.

  1. ID 컬럼: 숫자처럼 보이지만 선행 0이 있고 문자열로 다뤄야 함 ("001234")
  2. 금액/수량 컬럼: "1,234", "-", "N/A", 공백 포함
  3. 날짜 컬럼: 일부 행만 다른 포맷 ("2026-01-01" vs "01/01/2026")
  4. 코드 컬럼: 숫자/문자 혼합 (101, "A12")
  5. 불완전한 결측치: NA, NULL, null, 빈 문자열, -가 섞임

이런 컬럼을 “자동 추론”에 맡기면, 파이프라인이 커질수록 문제가 증폭됩니다. (예: DB 적재 시 스키마 불일치, 조인 키 타입 불일치)

1단계: 어떤 컬럼이 문제인지 빠르게 특정하기

경고에 나온 컬럼 인덱스 확인

경고 메시지에 Columns (x) 형태로 인덱스가 나옵니다. 인덱스를 컬럼명으로 바꾸려면 헤더를 읽어 확인합니다.

import pandas as pd

path = "data.csv"

# 헤더만 확인
cols = pd.read_csv(path, nrows=0).columns
print(list(enumerate(cols)))

문제 컬럼을 문자열로 읽고 값 분포 확인

문제 컬럼만 우선 string으로 읽으면, 어떤 값이 타입을 깨는지 찾기 쉽습니다.

import pandas as pd

path = "data.csv"

# 예: 3번 컬럼이 문제라고 가정
col = pd.read_csv(path, nrows=0).columns[3]

s = pd.read_csv(path, usecols=[col], dtype={col: "string"})[col]

print("null ratio:", s.isna().mean())
print("top values:\n", s.value_counts(dropna=False).head(20))

# 숫자로 변환 불가한 값 샘플
bad = pd.to_numeric(s, errors="coerce").isna() & s.notna()
print("bad samples:\n", s[bad].head(30))

이 단계만 해도 -, 1,234, N/A, " " 같은 “오염 값”이 바로 드러납니다.

2단계: low_memory=False는 해결책이 아니라 힌트

많이 알려진 처방은 다음입니다.

df = pd.read_csv("data.csv", low_memory=False)

이 옵션은 청크로 쪼개지 않고 한 번에 타입 추론을 하게 만들어 경고를 줄일 수 있습니다. 하지만 다음 한계가 있습니다.

  • 타입 추론 결과가 “정답”이라는 보장이 없음 (여전히 혼합 값이면 object로 떨어질 수 있음)
  • 큰 파일에서 메모리 사용량 급증
  • 경고가 사라져도 다운스트림에서 타입 문제는 계속 잠복

따라서 low_memory=False는 “조용히 넘어가기” 용도가 아니라, 타입 추론이 청크 간 충돌인지 확인하는 진단용으로 쓰는 편이 안전합니다.

3단계: 안전한 로딩의 핵심은 dtype 고정

가장 재현성과 안정성이 높은 방식은 처음부터 컬럼 타입을 명시하는 것입니다.

ID/코드 컬럼은 string으로 고정

선행 0이 의미를 갖는 키는 숫자로 읽는 순간 데이터가 변형됩니다.

import pandas as pd

dtype_map = {
    "user_id": "string",
    "order_id": "string",
    "zipcode": "string",
}

df = pd.read_csv("orders.csv", dtype=dtype_map)

숫자 컬럼은 “정제 후 변환”을 전제로 문자열로 읽기

오염이 심한 숫자 컬럼은 곧장 float로 읽기보다, 먼저 문자열로 읽고 정제한 뒤 변환하는 편이 안전합니다.

import pandas as pd

raw = pd.read_csv(
    "payments.csv",
    dtype={"amount": "string"},
    keep_default_na=False,  # 빈 문자열을 그대로 보존해 정제하기 쉽게
)

# 쉼표 제거, 공백 제거, 하이픈을 결측으로 처리
amount = (
    raw["amount"]
    .str.strip()
    .replace({"": pd.NA, "-": pd.NA, "N/A": pd.NA})
    .str.replace(",", "", regex=False)
)

raw["amount"] = pd.to_numeric(amount, errors="coerce")

이 패턴의 장점은 “어떤 값이 변환 실패했는지”를 재현 가능하게 추적할 수 있다는 점입니다.

4단계: 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", "NULL", "null"}:
        return pd.NA
    x = x.replace(",", "")
    return float(x)


df = pd.read_csv(
    "payments.csv",
    converters={"amount": parse_amount},
)

주의할 점은 converters는 파이썬 레벨 함수 호출이 많아져 대용량에서 느려질 수 있다는 것입니다. 속도가 중요하면 “문자열로 읽고 벡터화 정제”가 보통 더 빠릅니다.

5단계: 결측치 규칙을 명시해 타입 흔들림 막기

혼합 타입의 큰 원인은 결측치 표현이 제각각이기 때문입니다. na_valueskeep_default_na를 조합해 규칙을 고정하세요.

import pandas as pd

na_tokens = ["", " ", "-", "N/A", "NA", "NULL", "null"]

df = pd.read_csv(
    "data.csv",
    na_values=na_tokens,
    keep_default_na=False,
)
  • keep_default_na=False로 기본 NA 토큰 해석을 끄면, 내가 지정한 na_values가 더 예측 가능하게 동작합니다.
  • 단, 실제 데이터에서 "NA"가 코드 값이라면 결측으로 처리하면 안 됩니다. 이 경우 컬럼별로 다르게 처리해야 합니다.

6단계: 날짜 컬럼은 parse_dates + 실패 검증

날짜는 자동 파싱에 맡기면 지역/포맷 차이로 조용히 object가 되거나 잘못 해석될 수 있습니다.

import pandas as pd

# 우선 문자열로 읽고, 명시 포맷으로 변환하는 방식이 안전
raw = pd.read_csv("events.csv", dtype={"event_time": "string"})

raw["event_time"] = pd.to_datetime(
    raw["event_time"],
    format="%Y-%m-%d %H:%M:%S",
    errors="coerce",
)

bad = raw["event_time"].isna() & raw["event_time"].notna()
# 위 bad는 원본을 별도 보관해야 더 정확히 잡을 수 있음

포맷이 여러 개 섞였다면, 포맷별로 단계적으로 파싱하거나(예: ISO 먼저, 그다음 슬래시 포맷) 아예 수집 단계에서 포맷을 고정하는 것이 최선입니다.

7단계: 대용량 CSV는 청크 로딩 + 스키마 고정

파일이 매우 크면 한 번에 읽기 어렵습니다. 이때도 핵심은 “청크를 읽되 타입은 고정”입니다.

import pandas as pd

schema = {
    "user_id": "string",
    "order_id": "string",
    "amount": "string",  # 정제 후 numeric
}

chunks = pd.read_csv("big.csv", dtype=schema, chunksize=200_000)

out = []
for c in chunks:
    amt = (
        c["amount"].str.strip()
        .replace({"": pd.NA, "-": pd.NA})
        .str.replace(",", "", regex=False)
    )
    c["amount"] = pd.to_numeric(amt, errors="coerce")
    out.append(c)

df = pd.concat(out, ignore_index=True)

이 방식은 메모리 사용량을 통제하면서도, “청크마다 dtype이 달라지는 문제”를 원천 차단합니다.

8단계: 로딩 후 dtype 계약(Contract) 검증하기

ETL에서 가장 위험한 것은 “조용히 성공한 것처럼 보이는데 타입이 틀린 상태”입니다. 로딩 후에 최소한의 검증을 자동화하세요.

import pandas as pd

expected = {
    "user_id": "string",
    "amount": "float64",
}

for col, dt in expected.items():
    actual = str(df[col].dtype)
    if actual != dt:
        raise ValueError(f"dtype mismatch: {col} expected={dt} actual={actual}")

# 숫자 컬럼 결측/이상치 비율 체크
bad_ratio = df["amount"].isna().mean()
if bad_ratio > 0.01:
    raise ValueError(f"amount parse failure ratio too high: {bad_ratio}")

이런 “계약 검증”은 배치 작업에서 특히 중요합니다. 타입이 틀어지면 이후 단계에서 메모리 폭증이나 프로세스 강제 종료로 이어질 수 있는데, 그런 상황에서는 아래 글의 진단 관점도 함께 도움이 됩니다.

실전 권장 레시피: 혼합 타입 CSV 로딩 템플릿

아래는 “경고를 숨기지 않고, 스키마를 고정하고, 오염 값을 정제하고, 검증까지” 포함한 템플릿입니다.

import pandas as pd

PATH = "input.csv"

NA_TOKENS = ["", " ", "-", "N/A", "NA", "NULL", "null"]

SCHEMA = {
    "id": "string",
    "code": "string",
    "amount": "string",
    "created_at": "string",
}


def load_csv_safe(path: str) -> pd.DataFrame:
    df = pd.read_csv(
        path,
        dtype=SCHEMA,
        na_values=NA_TOKENS,
        keep_default_na=False,
    )

    # amount 정제
    df["amount"] = (
        df["amount"]
        .str.replace(",", "", regex=False)
        .str.strip()
    )
    df["amount"] = pd.to_numeric(df["amount"], errors="coerce")

    # created_at 정제
    df["created_at"] = pd.to_datetime(
        df["created_at"],
        errors="coerce",
        format="%Y-%m-%d %H:%M:%S",
    )

    # 계약 검증
    if str(df["id"].dtype) != "string":
        raise ValueError("id must be string")

    return df


df = load_csv_safe(PATH)
print(df.dtypes)

자주 묻는 선택지 정리

  • low_memory=False를 써도 되나요?

    • “경고를 줄이는 용도”로는 가능하지만, 타입 안정성은 dtype 명시가 정답에 가깝습니다.
  • 그냥 전부 object로 읽으면 안전한가요?

    • 로딩은 되지만, 숫자 연산/정렬/조인에서 비용이 커지고 오류가 늦게 터집니다. “초기에 규칙을 정하고 변환”이 장기적으로 안전합니다.
  • DB 적재까지 고려하면?

    • CSV 로딩 단계에서 스키마를 고정하고(문자열 우선), 정제 후 명확한 타입으로 변환한 뒤 적재하세요. 이 과정이 흔들리면 배치가 간헐적으로 실패하고, 원인 파악이 어려워집니다.

운영 환경에서 이런 “간헐적 실패”는 CI나 배치 자동화에서도 자주 문제를 키웁니다. 캐시나 실행 환경 차이로 재현이 어렵다면 아래 글의 디버깅 관점도 참고할 만합니다.

결론

DtypeWarning은 pandas가 까다로워서가 아니라, CSV가 현실적으로 더럽기 때문에 발생합니다. 중요한 포인트는 다음 3가지입니다.

  1. 경고를 숨기기보다 문제 컬럼을 특정한다
  2. dtype스키마를 고정하고, 필요한 컬럼은 문자열로 받아 정제 후 변환한다
  3. 로딩 직후 dtype/결측/변환 실패율을 검증해 파이프라인을 안정화한다

이 패턴을 적용하면 혼합 타입 CSV에서도 재현 가능한 데이터 로딩이 가능해지고, 분석/ML/DB 적재 어디로 이어지든 타입 문제로 흔들리지 않는 기반을 만들 수 있습니다.