データ分析の現場において、PythonのPandasライブラリは非常に強力なツールとして広く利用されています。
しかし、近年のビッグデータ化に伴い、数GBから数十GBに及ぶ大容量のCSVファイルを扱う機会が増えています。
標準的なread_csv関数でこれらの巨大なファイルを一度に読み込もうとすると、PCのメモリを使い果たして「MemoryError」が発生することが珍しくありません。
こうしたメモリ不足の問題をスマートに解決するための強力な武器が、Pandasに備わっているchunksize引数です。
本記事では、大容量データを効率的に処理するための分割読み込みテクニックと、実務で役立つ具体的な実装方法を詳しく解説します。
なぜ大容量データでメモリ不足が発生するのか
Pandasが大容量データの扱いに苦慮する最大の理由は、データをメモリ(RAM)上に一括して展開する仕組みにあります。
CSVファイルはディスク上ではテキスト形式で保存されていますが、Pandasが読み込む際にはデータ型に合わせてバイナリ形式に変換されます。
この変換過程で、元のCSVファイルのファイルサイズよりも数倍から十倍近いメモリ容量が必要になることが一般的です。
例えば、1GBのCSVファイルを読み込むために、8GB以上のメモリが消費されることも少なくありません。
物理的なメモリ容量を超えたデータを読み込もうとすると、OSがプロセスを強制終了させるため、プログラムが停止してしまいます。
このような状況を回避するためには、データを小さな塊(チャンク)に分けて、少しずつ処理を進めるアプローチが不可欠です。
Pandasのデフォルト動作とメモリ消費の関係
通常、pd.read_csv()を呼び出すと、Pandasはファイル全体を走査して全ての行をDataFrameオブジェクトとして構築します。
この動作は、データセットがメモリに収まるサイズであれば非常に高速で利便性が高いものです。
しかし、データ量が増大すると、DataFrameの構築自体がシステムのボトルネックへと変わります。
一度メモリが枯渇し始めると、スワップ領域の使用により処理速度が極端に低下し、最終的にはクラッシュに至ります。
chunksize引数を用いた分割読み込みの基本
read_csv関数の引数であるchunksizeを指定すると、戻り値は通常のDataFrameではなく「TextFileReader」というイテレータオブジェクトに変わります。
このイテレータを使用することで、指定した行数(チャンク)ごとにデータを逐次的に読み込むことが可能になります。
まずは、基本的なコードの書き方を見ていきましょう。
import pandas as pd
# 10,000行ごとに読み込む設定
chunk_size = 10000
reader = pd.read_csv("large_data.csv", chunksize=chunk_size)
# イテレータから順番にデータを取り出して処理
for i, chunk in enumerate(reader):
print(f"チャンク {i+1} の処理を開始します。")
# ここで各チャンク(DataFrame型)に対する処理を行う
print(chunk.head())
# ループの途中で終了する例(動作確認用)
if i == 2:
break
チャンク 1 の処理を開始します。
...(1~10000行目のデータ)...
チャンク 2 の処理を開始します。
...(10001~20000行目のデータ)...
チャンク 3 の処理を開始します。
...(20001~30000行目のデータ)...
このように、forループを利用して1万行ずつメモリにロードするため、どれほど巨大なファイルであってもメモリ消費量を一定に抑えることができます。
TextFileReaderオブジェクトの役割
chunksizeを指定した際に返されるTextFileReaderオブジェクトは、ファイルポインタを管理しています。
ループが回るたびに「次のN行」を読み込み、不要になった前のデータはガベージコレクションの対象となります。
これにより、数千万行のデータであっても、一般的なノートPCで十分に処理できる環境が整います。
実践的な分割処理のシナリオ
データを分割して読み込む場合、単に読み込むだけでなく、何らかの処理を行って結果を保存することが多いはずです。
ここでは、「フィルタリング」「集計」「新しいファイルへの保存」という3つの実践的なパターンを紹介します。
1. 特定の条件でデータをフィルタリングする
巨大なマスタデータから特定の条件に合致する行だけを抽出したい場合、チャンクごとにフィルタリングを行い、結果をリストに蓄積してから最後に結合します。
import pandas as pd
chunk_size = 50000
filtered_list = []
# chunksizeを指定して読み込み
reader = pd.read_csv("sales_history.csv", chunksize=chunk_size)
for chunk in reader:
# 特定の店舗ID(101)のデータのみを抽出
filtered_chunk = chunk[chunk['shop_id'] == 101]
filtered_list.append(filtered_chunk)
# 抽出した全てのチャンクを1つのDataFrameに結合
result_df = pd.concat(filtered_list, ignore_index=True)
print(f"抽出後のデータ件数: {len(result_df)}")
この方法であれば、抽出後のデータがメモリに収まるサイズであれば問題なく処理を完結させられます。
2. 大容量データの集計処理(合計値や平均値)
ファイル全体をメモリに載せずに合計値や件数を算出する場合、チャンクごとに中間結果を計算し、変数に加算していく手法をとります。
import pandas as pd
total_sales = 0
total_count = 0
reader = pd.read_csv("transaction_data.csv", chunksize=100000)
for chunk in reader:
# 各チャンクの合計と件数を累計
total_sales += chunk['amount'].sum()
total_count += chunk['amount'].count()
average_sales = total_sales / total_count
print(f"総売上: {total_sales}, 平均単価: {average_sales:.2f}")
このロジックを用いれば、テラバイト級のデータであっても、メモリをほとんど消費せずに統計量を算出することが可能です。
3. 分割したデータの加工と新規保存
読み込んだデータを加工して新しいCSVファイルとして保存したい場合、to_csv関数のmode=’a’(追記モード)を活用します。
import pandas as pd
import os
input_file = "huge_input.csv"
output_file = "processed_output.csv"
chunk_size = 20000
# 出力ファイルが既に存在する場合は削除しておく
if os.path.exists(output_file):
os.remove(output_file)
reader = pd.read_csv(input_file, chunksize=chunk_size)
for i, chunk in enumerate(reader):
# データ加工(例:消費税計算カラムの追加)
chunk['tax_amount'] = chunk['price'] * 0.1
# 初回ループ時のみヘッダーを出力、以降は追記
if i == 0:
chunk.to_csv(output_file, index=False, mode='w')
else:
chunk.to_csv(output_file, index=False, mode='a', header=False)
print("処理が完了し、ファイルが保存されました。")
この方法により、入出力の両方でメモリ消費を最小限に抑えたパイプラインを構築できます。
chunksize利用時のパフォーマンス最適化テクニック
分割読み込みはメモリ不足を解消しますが、処理の設計次第では実行速度が低下することもあります。
ここでは、効率を最大化するためのポイントを整理します。
適切なチャンクサイズの決定方法
チャンクサイズが小さすぎると、ループのオーバーヘッド(関数の呼び出し回数やファイルアクセスの回数)が増大し、処理時間が延びてしまいます。
逆に大きすぎると、結局メモリを圧迫してしまいます。
一般的には、以下の表を目安に調整することをお勧めします。
| 利用可能メモリ | 推奨チャンクサイズ(行数) | 用途 |
|---|---|---|
| 8GB | 10,000 〜 50,000 | 一般的な事務用PCでの軽量処理 |
| 16GB | 100,000 〜 500,000 | 開発用ワークステーションでの並列処理 |
| 32GB以上 | 1,000,000以上 | 大規模なデータマイニング |
まずは10,000行程度から開始し、メモリ使用量に余裕があればサイズを大きくしていくのが最も安全なアプローチです。
dtypeの指定によるメモリ節約の併用
chunksizeと組み合わせて絶大な効果を発揮するのが、dtype引数によるデータ型の明示的指定です。
Pandasはデフォルトでデータ型を推論しますが、これが非常にメモリを贅沢に使う傾向があります。
例えば、整数のカラムにint64が割り当てられている場合、これをint32やint16に指定するだけで、そのカラムが消費するメモリを半分以下に減らせます。
# データ型を指定して読み込みをさらに効率化
data_types = {
'user_id': 'int32',
'rating': 'int8',
'category': 'category'
}
reader = pd.read_csv("ratings.csv", chunksize=100000, dtype=data_types)
特に「category」型は、種類が限られた文字列カラムに対して劇的なメモリ節約効果をもたらします。
分割読み込みを行う際も、各チャンクのメモリ占有率が下がるため、より大きなチャンクサイズを設定できるようになります。
usecols引数による列の絞り込み
不要なカラムを読み込まないことも、大容量データ処理の鉄則です。
usecols引数を使用して、分析に必要な最小限の列だけを読み込むようにしましょう。
これにより、ディスクI/Oとメモリ消費の両方を削減できます。
Pandas以外の選択肢と将来的な展望
2026年現在、Pandasの分割読み込みは依然として標準的な手法ですが、さらに大規模なデータを扱う場合には他のライブラリや形式との連携も検討に値します。
たとえば、CSV形式ではなくParquet形式を利用することで、データの読み込み速度と圧縮率を飛躍的に向上させることができます。
Parquetは列指向のフォーマットであるため、特定のカラムだけを抽出する処理が極めて高速です。
Parquet形式への変換例
# 一度だけchunksizeを使ってParquetに変換しておく
reader = pd.read_csv("massive_data.csv", chunksize=500000)
for i, chunk in enumerate(reader):
chunk.to_parquet(f"data_part_{i}.parquet")
一度Parquet形式に変換してしまえば、次回以降はさらに効率的なデータアクセスが可能になります。
また、並列処理が得意な「Polars」や、分散処理を行う「Dask」といったライブラリも、Pandasのchunksizeで限界を感じた際の強力な代替案となります。
まとめ
Pandasのchunksize引数は、大容量データ処理における「メモリ不足」という最大の壁を突破するための最も基本的かつ重要な機能です。
データを一括で読み込むのではなく、適切なサイズの塊(チャンク)に分けて逐次処理することで、限られたリソースを最大限に活用できます。
本記事で紹介したフィルタリング、集計、追記保存といったテクニックを組み合わせることで、実務におけるデータ処理の幅は大きく広がるはずです。
また、dtypeの最適化やusecolsによる列の限定を併用することで、さらにパフォーマンスを高めることができます。
まずは手元の環境でchunksizeを試し、データの規模に応じた最適な処理フローを構築してみてください。
適切なメモリ管理を行うことは、データサイエンティストやエンジニアとしての信頼性を高める重要なステップとなるでしょう。
