pandas read_csv가 파일 크기의 3배를 먹는 이유와 해결법

pandas read_csv가 파일 크기의 3배씩 메모리를 먹는 이유가 궁금해서 검색하셨다면, 원인은 크게 두 가지입니다. 문자열 컬럼이 object dtype으로 저장되면서 파이썬 객체 오버헤드가 그대로 붙고, 숫자 컬럼도 실제 필요한 범위보다 훨씬 큰 int64·float64로 자동 지정되기 때문입니다. dtype을 미리 지정하고 필요하면 chunksize로 나눠 읽으면 이 부풀림을 상당 부분 줄일 수 있습니다.

이 글은 pandas 2.x, CPython 3.10 이상 환경을 기준으로 설명합니다. 버전이 다르면 기본 엔진이나 메모리 계산 방식이 다를 수 있습니다.

왜 pandas read_csv가 파일 크기의 3배까지 메모리를 먹을까요?

CSV 파일은 텍스트라서 숫자든 문자든 모두 문자 그대로 저장돼 있습니다. 반면 pandas는 이걸 읽으면서 파이썬 객체와 numpy 배열로 바꾸는데, 이 변환 과정에서 원본보다 훨씬 큰 자료구조가 만들어집니다.

가장 큰 원인은 문자열 컬럼입니다. dtype을 따로 지정하지 않으면 문자열 컬럼은 object dtype이 되는데, 이건 numpy 배열에 파이썬 str 객체를 가리키는 포인터만 저장하는 방식입니다. 문자열 하나하나가 별도의 파이썬 객체이다 보니 짧은 문자열이라도 객체 자체의 오버헤드가 상당히 붙습니다. “서울”, “부산” 같은 두세 글자 문자열도 실제 메모리에서는 그보다 훨씬 큰 용량을 차지합니다.

숫자 컬럼도 마찬가지입니다. CSV에 1~100 사이의 값만 들어 있어도 pandas는 기본적으로 int64로 읽습니다. int8이면 1바이트로 충분한 값을 8바이트짜리 정수로 저장하는 셈입니다. 여기에 C 파서가 파싱 중 만드는 임시 버퍼까지 겹치면, 파일 크기의 2~3배까지 메모리가 순간적으로 늘어나는 상황이 드물지 않습니다.

dtype 지정으로 메모리 사용량을 줄이는 원리

read_csv의 dtype 파라미터에 컬럼별 타입을 딕셔너리로 넘기면, pandas가 자동 추론을 건너뛰고 지정한 타입으로 바로 읽습니다. 정수 범위가 작으면 int8·int16·int32로, 소수점 정밀도가 크게 중요하지 않으면 float32로 낮출 수 있습니다.

반복되는 값이 많은 문자열 컬럼은 category dtype이 효과적입니다. 예를 들어 주문 상태값이 “대기”, “완료”, “취소” 몇 가지뿐이라면, category dtype은 실제 값을 한 번씩만 저장하고 각 행에는 정수 코드만 매핑합니다. 고유값 종류가 적을수록 절약 폭이 커집니다.

파이썬 데이터 분석 코드가 실행 중인 노트북 화면

dtype_map = {
    'order_id': 'int32',
    'customer_id': 'int32',
    'status': 'category',
    'amount': 'float32',
}
df = pd.read_csv('sales.csv', dtype=dtype_map)

값의 범위를 미리 모른다면 pd.to_numeric(series, downcast=’integer’)로 먼저 확인해보고 다운캐스트 가능한 최소 타입을 찾는 방법도 있습니다. pandas 공식 문서의 Scaling to large datasets 가이드에서 dtype 지정과 category 활용 예시를 자세히 다루고 있습니다.

dtype 적용 전후 메모리 사용량이 이렇게 달라집니다

메모리 사용량은 df.memory_usage(deep=True)로 확인합니다. deep=True를 빼면 object 컬럼은 포인터 크기만 계산되어 실제보다 훨씬 작게 나오니 주의가 필요합니다.

import pandas as pd

df = pd.read_csv('sales.csv')  # dtype 자동 추론
print(df.dtypes)
print(df.memory_usage(deep=True).sum() / 1024**2, 'MB')

가령 주문 100만 건, 컬럼 4개(주문번호, 고객번호, 상태, 금액)짜리 CSV라면 파일 자체는 60~70MB 수준이어도 위 코드의 결과는 180MB 안팎으로 나오는 경우가 많습니다. 파일보다 2배 이상 커지는 거죠.

dtype_map = {
    'order_id': 'int32',
    'customer_id': 'int32',
    'status': 'category',
    'amount': 'float32',
}
df2 = pd.read_csv('sales.csv', dtype=dtype_map)
print(df2.memory_usage(deep=True).sum() / 1024**2, 'MB')

같은 데이터를 dtype 지정 후 읽으면 60~70MB 수준으로 떨어지는 걸 확인할 수 있습니다. status 컬럼의 고유값이 적을수록, 정수 컬럼의 범위가 작을수록 절약 폭은 더 커집니다. 정확한 수치는 데이터 분포에 따라 달라지니, 직접 memory_usage(deep=True)로 재보시는 게 가장 확실합니다.

chunksize로 나눠 읽어야 하는 상황

dtype을 최적화해도 파일 자체가 너무 커서 메모리에 한 번에 못 올리는 경우가 있습니다. 이럴 땐 chunksize를 써서 파일을 조각내 순차적으로 처리합니다.

chunks = pd.read_csv('sales.csv', chunksize=200_000, dtype=dtype_map)

refunded_total = 0
for chunk in chunks:
    refunded_total += chunk.loc[chunk['status'] == '취소', 'amount'].sum()

print(refunded_total)

대용량 데이터 처리와 메모리 과부하를 나타내는 RAM 칩

chunksize를 지정하면 read_csv는 DataFrame이 아니라 TextFileReader 이터레이터를 반환합니다. for문으로 순회할 때마다 지정한 행 수만큼만 DataFrame이 만들어지고, 이전 청크는 처리가 끝나면 가비지 컬렉션 대상이 됩니다. 전체를 한 번에 올리지 않으니 파일이 램 용량보다 커도 집계 작업이 가능합니다.

다만 groupby나 정렬처럼 전체 데이터를 봐야 하는 연산은 청크별로 나눠서 처리한 뒤 다시 합치는 로직을 직접 짜야 합니다. 단순 필터링·합산은 위 코드처럼 바로 되지만, 중복 제거나 전역 정렬은 훨씬 손이 많이 갑니다.

dtype을 지정해도 메모리가 안 줄어드는 경우가 있습니다

정수 다운캐스트는 값의 범위를 벗어나면 위험합니다. int8로 지정했는데 나중에 CSV에 200이 넘는 값이 들어오면 파싱 단계에서 오류가 나거나, 계산 도중 오버플로가 생길 수 있으니 데이터의 min·max를 먼저 확인하는 게 안전합니다.

category dtype도 만능은 아닙니다. 주문번호나 고객 ID처럼 고유값 종류가 행 수만큼 많은 컬럼에 category를 쓰면, 코드-값 매핑 테이블만 커져서 오히려 object dtype보다 손해를 볼 수 있습니다. 카디널리티가 낮은 컬럼에만 적용하는 게 좋습니다.

read_csv의 low_memory 옵션도 헷갈리는 부분입니다. 기본값은 True인데, 이름과 달리 메모리를 줄여주는 옵션이 아닙니다. 파일을 내부적으로 청크 단위로 읽으면서 dtype을 추론하고 나중에 합치는 방식이라, 청크마다 추론된 dtype이 다르면 “mixed types” 경고가 뜹니다. low_memory=False는 파일 전체를 한 번에 읽어 dtype 일관성은 보장하지만, 그 과정에서 메모리를 더 많이 쓰기도 합니다. 두 옵션 모두 dtype을 직접 지정하는 것의 대체재는 아닙니다.

병목이 메모리가 아니라 디스크 I/O라면 dtype·chunksize 조정만으로는 체감 속도가 크게 안 바뀝니다. 이런 경우엔 CSV를 Apache Parquet 같은 컬럼 지향 포맷으로 한 번 변환해두고 이후부터는 parquet 파일을 읽는 방식이 더 근본적인 해결책입니다.

지금 내 CSV 파일에 적용해 볼 순서

pandas read_csv가 파일 크기의 3배를 먹는 현상은 대부분 object dtype의 문자열 오버헤드와 필요 이상으로 큰 숫자 타입 때문입니다. df.memory_usage(deep=True)로 현재 상태를 먼저 재보고, 컬럼별로 적절한 dtype 맵을 만들어 read_csv에 넘기는 것부터 시작하시면 됩니다.

그래도 메모리가 부족하면 chunksize로 나눠 읽으면서 필요한 값만 집계하는 방식으로 전환하시고, 반복 작업이 잦은 데이터라면 아예 parquet으로 변환해두는 걸 검토해보시기 바랍니다.

AI 개발자를 위한 필수 라이브러리, NumPy와 Pandas 제대로 쓰는 법

Leave a Comment