「CSVを読み込んだらメモリを大量に使ってしまい、他の処理が重くなった」——pandasでファイルサイズ以上にメモリを消費してしまうのは、よくあるつまずきです。原因の多くは「読み込み方」にあり、指定を変えるだけで大きく改善できます。本記事では、実際にコードを動かしてメモリ使用量を測定しながら、削減方法を順番に確認していきます。
注意(バージョンによる違い): pandas 3.0 から文字列列の既定型が object から str に変わりました。そのため本記事の数値は pandas 2.x とは傾向が異なります(後述)。手元の環境で試す場合は python -c "import pandas; print(pandas.__version__)" でバージョンを確認してください。
1. なぜ大きなCSVでメモリが問題になるのか
pd.read_csv() は既定ですべての列をメモリに読み込みます。行数が数十万を超えると、「ファイルサイズは数十MBなのに、読み込むとその数倍のメモリを消費する」という状態になりがちです。原因は主に2つあります。
- 数値列が必要以上に大きな型で読まれる — 1〜5 しか入らない列でも既定では64bit整数(
int64)になります - 同じ文字列が何度も保持される — 「新宿店」のように繰り返し現れる値も、行ごとに文字列として保持されます
どちらも「読み込み方」を指定するだけで改善できます。まず現状を測定してみます。
2. まず現状を測る
import pandas as pd
df = pd.read_csv("sales.csv")
# deep=True で文字列の実体サイズまで含めて集計する
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")
print(df.dtypes)手元の環境(pandas 3.0.5)での実行結果です。
56.8 MB
order_id int64
shop str[python]
category str[python]
amount int64
qty int64
dtype: objectpandas 3.0 以降は文字列列が既定で str 型になっており、2.x の object 型よりもとから多少効率的です。2.x 環境では、この時点でのメモリ使用量がもっと大きくなります。
参照: DataFrame.memory_usage — pandas ドキュメント
3. 対策1: dtype を指定する
列に入る値の範囲が分かっているなら、型を明示します。qty が1〜5しか取らないなら int8(-128〜127)で十分で、int64 の8分の1のサイズになります。
df = pd.read_csv(
"sales.csv",
dtype={"order_id": "int32", "amount": "int32", "qty": "int8"},
)
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")52.5 MB数値列だけを32bit/8bitに絞った段階では、文字列列(shop・category)がまだ大部分を占めているため、削減幅はまだ小さめです。
4. 対策2: 繰り返しの多い文字列は category 型に
category 型は、出現する値の一覧を1つ持ち、各行にはその番号だけを保持します。「店舗名」「都道府県」「ステータス」のように取りうる値の種類が少なく、行数が多い列で効果が大きくなります。逆に、ほぼ全行が異なる値の列(メールアドレスなど)ではかえって非効率になります。
df = pd.read_csv(
"sales.csv",
dtype={
"order_id": "int32", "amount": "int32", "qty": "int8",
"shop": "category", "category": "category",
},
)
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")3.1 MBここで大きく減りました。52.5 MB → 3.1 MB です。今回の検証データでは「shop」「category」列がそれぞれ4種類の値しか持たないため、category型の効果が最大限に出ています。
参照: Categorical data — pandas ユーザーガイド
5. 対策3: 使う列だけ読む(usecols)
集計に2列しか使わないなら、最初から2列だけ読みます。読み込み時間も短くなります。
df = pd.read_csv(
"sales.csv",
usecols=["shop", "amount"],
dtype={"amount": "int32", "shop": "category"},
)
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")1.4 MB6. それでも載らないときは chunksize で分割する
chunksize を指定すると、read_csv はDataFrameではなく「かたまりを順に返すイテレータ」を返します。一度に保持するのは1かたまり分だけなので、ファイル全体がメモリに載らない場合でも集計できます。
total = None
for chunk in pd.read_csv(
"sales.csv",
chunksize=50_000,
usecols=["shop", "amount"],
dtype={"amount": "int32", "shop": "category"},
):
s = chunk.groupby("shop", observed=True)["amount"].sum()
total = s if total is None else total.add(s, fill_value=0)
print(total)shop
品川店 753718179
新宿店 751528513
池袋店 753892615
渋谷店 760104071
Name: amount, dtype: int64注意点はかたまりごとの結果を自分で合算する必要があることです。合計や件数のように「分割して足せる」集計は素直に書けますが、中央値のように全体を見ないと出せない集計はこの方法に向きません。
参照: Scaling to large datasets — pandas ユーザーガイド / read_csv — pandas ドキュメント
7. まとめ
読み込み方 | メモリ使用量 |
|---|---|
指定なし | 56.8 MB |
dtype 指定 | 52.5 MB |
+ category 型 | 3.1 MB |
usecols で2列に絞る | 1.4 MB |
今回の検証データ(30万行、繰り返しの多い文字列列)では、category型の指定が最も効果が大きく、56.8 MBから3.1 MBまで削減できました。ただし数値は検証データの性質(列の種類数・行数)に強く依存するため、実際の業務データで試す際は、まずdf.memory_usage(deep=True)で現状を測ることから始めてください。
この記事は PythonとPandasでデータクリーニングを自動化 の続編です。データの前処理を自動化した後、その処理が重くなってきたら、今回の方法を組み合わせてみてください。