HOWTO · Pandas

Pandas에서 chunksize 사용하기

이 튜토리얼에서는 Python의 chunksize 매개변수에 대해 설명합니다.

이 페이지의 내용

pandas 라이브러리는 데이터를 행과 열로 구성된 DataFrame으로 정리하며, read_csv와 같은 리더는 보통 파일 전체를 하나의 DataFrame으로 읽어 들입니다. 파일이 부담 없이 로드하기에는 너무 큰 경우 — 예를 들어 수백만 개의 행이 담긴 MovieLens 평점 데이터 세트처럼 — chunksize 매개변수를 사용하면 파일을 행 단위 배치로 나누어 읽고 처리할 수 있습니다.

Pandas에서 chunksize가 동작하는 방식

지원되는 pandas 리더에 chunksize=n을 전달하면 반환 값이 달라집니다. DataFrame 대신 read_csv()는 TextFileReader를 반환하며, 이 객체는 배치마다 하나의 DataFrame을 생성하는 반복자와 유사한 객체입니다. 각 배치에는 최대 n개의 행이 들어 있고, 행 수가 청크 크기의 정확한 배수가 아니면 마지막 배치에는 더 적은 행이 들어갈 수 있습니다.

이 패턴을 적용할 때는 두 가지 세부 사항이 중요합니다. 첫째, chunksize는 바이트가 아니라 행을 계산합니다. 모든 청크는 파일의 모든 열을 그대로 유지하며, 달라지는 것은 행 수뿐입니다. 둘째, 이 매개변수는 그 자체로 파싱 속도를 높여 주지 않습니다. 메모리가 할당되는 시점을 바꿀 뿐이므로, 실제로 도움이 되는지는 프로그램이 각 청크에서 무엇을 유지하느냐에 따라 달라집니다.

CSV 파일을 청크로 읽기

파일을 배치로 읽으려면 read_csv에 chunksize를 전달하고 결과를 순회합니다. 아래 예제는 작은 평점 형식의 CSV를 임시 파일에 작성한 다음, 한 번에 세 행씩 다시 읽으면서 생성되는 각 DataFrame의 shape를 출력하여 배치 처리 과정을 보여줍니다.

다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import os
import tempfile

import pandas as pd

rows = ["userId,movieId,rating"]
for i in range(7):
    rows.append(f"{i + 1},{100 + i},{1.0 + (i % 5) * 0.5}")

path = os.path.join(tempfile.mkdtemp(), "ratings.csv")
with open(path, "w") as f:
    f.write("\n".join(rows) + "\n")

reader = pd.read_csv(path, chunksize=3)
for number, chunk in enumerate(reader, start=1):
    print(f"chunk {number}: shape={chunk.shape}")

아래 출력의 chunk와 shape 레이블은 예제가 영어 문자열을 그대로 출력하므로 영어로 표시됩니다.

chunk 1: shape=(3, 3)
chunk 2: shape=(3, 3)
chunk 3: shape=(1, 3)

파일에는 일곱 개의 데이터 행이 있고 chunksize=3이므로, TextFileReader는 두 개의 가득 찬 청크에 이어 한 행짜리 나머지 청크를 생성합니다. 열 수는 모든 청크에서 세 개로 유지되며, 이는 배치 처리가 행에만 적용된다는 것을 확인해 줍니다.

각 청크 처리하기

유용한 패턴은 청크 자체를 모두 모아 두는 대신 청크마다 작은 결과 — 개수, 합계 또는 필터링된 부분 집합 — 를 누적하는 것입니다. 이 예제는 같은 파일을 집계하면서 누적 행 수와 평점 합계를 추적하므로, 전체 파일이 한꺼번에 메모리에 올라갈 필요가 없습니다.

다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import os
import tempfile

import pandas as pd

rows = ["userId,movieId,rating"]
for i in range(7):
    rows.append(f"{i + 1},{100 + i},{1.0 + (i % 5) * 0.5}")

path = os.path.join(tempfile.mkdtemp(), "ratings.csv")
with open(path, "w") as f:
    f.write("\n".join(rows) + "\n")

total_rows = 0
rating_sum = 0.0
for chunk in pd.read_csv(path, chunksize=3):
    total_rows += len(chunk)
    rating_sum += chunk["rating"].sum()

print(f"rows read: {total_rows}")
print(f"average rating: {rating_sum / total_rows:.2f}")

이 예제의 출력 레이블(rows read, average rating)도 예제가 영어 문자열을 그대로 출력하므로 영어로 표시됩니다.

rows read: 7
average rating: 1.79

루프 안에서는 필터링, 열 선택, groupby 집계 등 어떤 DataFrame 연산도 사용할 수 있으므로, 각 청크를 누적 결과에 반영하기 전에 원하는 대로 변환할 수 있습니다.

청크 크기와 메모리 트레이드오프 이해하기

청크로 읽으면 최대 메모리 사용량을 줄일 수 있지만, 그 효과가 자동으로 생기는 것은 아닙니다. 최대 사용량은 대략 가장 큰 청크의 크기에 프로그램이 유지하는 데이터의 양을 더한 정도입니다. 모든 청크를 목록에 추가해 두었다가 나중에 연결하면 전체 데이터 세트를 다시 메모리에 보관하게 되어, 한 번에 읽는 일반적인 방식보다 더 많은 메모리를 사용할 수도 있습니다.

두 가지 압력의 균형을 맞추어 값을 선택하세요. chunksize 값이 크면 루프 반복 횟수가 줄고 반복당 오버헤드도 적어지지만, 각 청크가 차지하는 메모리는 늘어납니다. 값이 작으면 최대 사용량은 낮아지지만 루프를 더 많이 통과해야 합니다. 또한 마지막 청크는 보통 더 짧으므로, 고정된 행 수를 가정하지 않는 루프 코드를 작성하세요.

다른 데이터 소스에서 청크 읽기 사용하기

chunksize는 read_csv에만 국한되지 않습니다. 이 매개변수를 허용하는 다른 pandas 리더로는 pandas.read_json(lines=True와 함께), pandas.read_sql 및 pandas.read_sql_table, pandas.read_stata, pandas.read_sas 등이 있으며, pandas.read_hdf와 같은 일부 리더는 대신 이에 상응하는 iterator 모드를 제공합니다. 지원 여부와 허용되는 옵션은 리더와 pandas 버전에 따라 다르므로, 사용하려는 함수의 공식 문서를 확인하세요.

청크 읽기가 가능한 곳이면 어디서나 패턴은 동일합니다. 배치를 요청하고, 배치를 순회하며, 각 배치를 작은 결과로 결합하면 됩니다.