[개발 깨알 상식_Tips] / CSV 식별자 컬럼의 숫자 추론 오류.md

CSV 식별자 컬럼의 숫자 추론 오류

조회

2026년 6월 17일 | 개발 깨알 상식_Tips


CSV 안의 고객 ID나 우편번호는 숫자처럼 보여도, 실제로는 계산할 값이 아니라 표기 자체가 의미인 문자열인 경우가 많다. 그런데 pandas.read_csv를 아무 옵션 없이 부르면 이 컬럼이 자연스럽게 정수나 실수로 추론되고, 00123123이 된다. 나는 이 문제를 꽤 늦게까지 “나중에 포맷팅하면 되겠지” 정도로 넘겼는데, 조인 키가 한 번 바뀌고 나면 그 뒤의 오류는 훨씬 찾기 어려워진다. 그래서 CSV를 읽을 때는 숫자처럼 보이는 식별자 컬럼부터 먼저 고정해 둔다.

CSV 식별자 컬럼을 dtype map으로 먼저 고정하는 흐름
숫자형 추론은 편하지만, ID·코드·우편번호처럼 표기 자체가 값인 컬럼에는 먼저 브레이크를 걸어야 한다.

왜 읽는 순간에 고쳐야 하나

앞자리 0이 사라지는 문제는 화면 출력만의 문제가 아니다. CSV 원본에서는 00123123이 다른 코드였는데, 로딩 뒤 둘 다 같은 값처럼 보이면 이미 정보가 접힌 상태다. 나중에 str.zfill(5)를 붙여 복구할 수 있는 경우도 있지만, 원래 길이가 5자리였는지 6자리였는지, 비어 있던 값이 어떤 뜻이었는지 모르면 복구가 추측이 된다. 특히 고객 ID, 사업장 코드, 우편번호, 상품 SKU처럼 다른 테이블과 붙는 키는 한 번 틀어지면 merge 결과의 행 수가 조용히 달라진다.

그래서 나는 CSV를 읽은 뒤 dtype을 확인하는 것보다, 읽기 전에 작은 schema를 적는 쪽을 선호한다. 전체 컬럼을 다 고정할 필요는 없다. 숫자로 계산할 컬럼은 놔두고, 식별자·코드·전화번호·우편번호처럼 문자열이어야 하는 컬럼만 먼저 지정하면 된다.

dtype map을 작게 적어 두기

가장 단순한 형태는 dtype에 dict를 넘기는 것이다. pandas 문서에서도 dtype은 컬럼별 자료형을 지정하는 인자로 설명되어 있고, 이런 식으로 일부 컬럼만 문자열로 고정할 수 있다.

import pandas as pd

dtype_map = {
    "customer_id": "string",
    "zip_code": "string",
    "phone": "string",
}

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

여기서 중요한 건 멋진 타입 시스템을 만드는 것이 아니라, 자동 추론이 건드리면 안 되는 컬럼을 표시하는 것이다. 나는 처음 보는 CSV를 다룰 때 파일 첫 줄 몇 개를 보고 바로 읽기보다, 컬럼 이름 중에 id, code, zip, phone, sku가 들어간 것부터 따로 적어 둔다. 이 정도만 해도 “앞자리 0이 왜 사라졌지” 같은 디버깅은 꽤 줄어든다.

문자열로 읽으면 메모리가 조금 더 들 수 있다는 걱정도 있다. 그 걱정은 맞지만, 식별자 컬럼 몇 개 때문에 전체 파이프라인을 불안하게 만들 필요는 없다. 정말 큰 파일이라면 처음 1만 줄 정도만 읽어 후보 컬럼을 고르고, 필요한 컬럼만 usecols로 좁힌 뒤 schema를 붙이는 식으로 시작한다. 성능 최적화는 나중에도 할 수 있지만, 원본 코드가 숫자로 접혀 사라진 뒤에는 확인 가능한 근거가 줄어든다.

빈 값과 NA 문자열은 따로 보기

식별자 컬럼에서 더 헷갈리는 지점은 빈 값이다. pandas는 기본적으로 여러 문자열을 결측값 후보로 해석한다. 이 동작은 분석용 숫자 컬럼에는 편하지만, 코드 컬럼에서는 NA라는 실제 지역 코드나 상품 코드가 결측으로 바뀌는 식의 오해를 만들 수 있다. 이럴 때는 keep_default_na=False를 함께 검토한다.

df = pd.read_csv(
    "customers.csv",
    dtype={"customer_id": "string", "zip_code": "string"},
    keep_default_na=False,
)

물론 모든 결측 처리를 꺼 버리는 게 정답은 아니다. 금액이나 수량처럼 진짜 비어 있으면 결측으로 읽어야 하는 컬럼도 있다. 그래서 내 기준은 단순하다. 식별자 컬럼의 표기 보존과 분석 컬럼의 결측 처리를 같은 규칙으로 묶지 않는다. 필요하면 na_values를 컬럼별로 따로 주고, 식별자 쪽은 문자열 보존을 우선한다.

converters는 마지막에 꺼내기

가끔은 dtype만으로 부족하다. 예를 들어 공백을 제거하고, 하이픈을 통일하고, 길이가 짧은 코드를 왼쪽 0으로 채워야 하는 규칙이 있으면 converters가 편하다. 다만 나는 이것을 첫 번째 카드로 쓰지는 않는다. 변환 함수가 들어가면 “읽기” 단계와 “정규화” 단계가 한 줄에 섞이기 때문이다.

def normalize_code(value):
    value = str(value).strip()
    return value.zfill(5) if value else value

df = pd.read_csv(
    "stores.csv",
    converters={"store_code": normalize_code},
)

이 방식은 강력하지만, 원본이 이미 숫자로 접힌 뒤에는 늦을 수 있다. 또 변환 규칙 자체가 데이터 품질 판단을 포함하기 때문에, 팀 작업에서는 함수 이름과 테스트 샘플을 남겨 두는 편이 좋다. 단순 보존은 dtype, 규칙이 있는 정규화는 converters로 나누면 읽는 사람도 의도를 따라가기 쉽다.

내가 남기는 작은 체크

CSV를 읽은 직후에는 전체 통계를 보기 전에 식별자 컬럼 몇 개만 먼저 확인한다. 앞자리 0이 있는 샘플, 빈 문자열 샘플, 길이가 예상보다 긴 샘플을 각각 하나씩 찍어 보는 식이다. 이때 행 수나 평균보다 먼저 보는 것은 dtype과 대표 값이다. 데이터가 크지 않다면 아래처럼 아주 작은 확인이면 충분하다.

for col in ["customer_id", "zip_code", "phone"]:
    print(col, df[col].dtype, df[col].head(3).tolist())

이 체크는 화려하지 않지만, 데이터 파이프라인 초반의 실패를 꽤 일찍 잡아 준다. 숫자형 추론은 pandas의 장점이고, 매번 끄고 시작할 필요는 없다. 다만 ID처럼 값의 크기보다 값의 모양이 중요한 컬럼은 다르다. 이런 컬럼은 계산 전에 의미가 이미 정해져 있으니, CSV를 읽는 순간부터 그 의미를 지키는 쪽이 안전하다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.