pandas 3.0.0rc2 では CSV 書き出しが 3402.5ms かかった
この記事の見出し
pandas 3.0.0rc2 で2000000行の DataFrame を測ったら、to_csv の中央値が3402.5ms、read_csv が882.7ms になった。
重かったのは集計そのものではなく CSV の入出力で、groupby は238.1ms、sort_values は470.0ms で終わっている(処理本体より出口が重い)。対して CSV 書き出しは読み込みの3.9倍で、データ処理よりもファイルへ出す段階のほうがずっと高い(ここを見落とすと、速くする場所を取り違える)。
想定より軽かったのは、2000000行という大きさでも、処理の中身だった。value_counts は6.1ms、条件で絞って合計する処理も26.1ms だった。Surface Pro 11th Edition の Snapdragon X Elite X1E80100 で、pandas が普通に走っているだけなら、この部分は詰まりにくい。
計測日は2026年7月22日で、Python は ARM64 版、pandas は3.0.0rc2、NumPy は2.4.1、matplotlib は3.10.8 を使った(この組み合わせでの記録)。pandas 3.0.0rc2 はリリース候補版なので、安定版の代表値として扱うには注意が必要になる。2026-07-22 時点のこの環境ではこうだった、という記録にとどめる。
DataFrame とファイルを同じ流れで測る
2000000行、3列の DataFrame を作り、同じ処理を複数回まわして中央値を見た。列はカテゴリ的に使う列、数値として集計する列、並べ替えに使う列という素朴な構成にしている。実際に測った対象は、groupby、sort_values、value_counts、フィルタ後の合計、CSV 書き出し、CSV 読み込み、matplotlib の PNG 保存である(仕事でよく並ぶ処理だけを、かなり雑に横へ置いた形)。
スクリプトの中心はこういう形だった。
cases = {
"groupby_mean": lambda: df.groupby("key")["value"].mean(),
"sort_values": lambda: df.sort_values("score"),
"value_counts": lambda: df["key"].value_counts(),
"filter_sum": lambda: df.loc[df["score"] > 0.5, "value"].sum(),
}
まずは単純に測ってみることにして、なぜここまで CSV だけが目立つのかを後から考えるにしても、最初に同じ DataFrame から横並びで数字を取っておかないと、感覚だけで犯人を決めてしまう。
CSV は一度ファイルに出してから、同じファイルを読み直している。ここをメモリ内の文字列にしてしまうと、普段の中間ファイル運用から外れるので、今回はファイルを経由させた(きれいなベンチより、手元で起きる待ち時間を優先した)。
df.to_csv(csv_path, index=False)
pd.read_csv(csv_path)
PNG は matplotlib で折れ線を描いて保存した。点数まで JSON に残していないので、この記事では PNG 出力の時間とサイズだけを見る。そこを後から思い出しで補うと、数字の出どころが混ざってしまう。
CSV だけ秒単位になる
全反復値をそのまま置く(中央値だけで揃ったように見せないため)。csv_bytes と png_bytes は、該当しない処理にも同じ測定ファイルのサイズとして並べている。表の rows は DataFrame の行数で、matplotlib の描画点数ではない(この表だけ見ると少し紛らわしいが、JSON の列をそのまま残した)。
| ケース | runs_ms | median_ms | rows | csv_bytes | png_bytes |
|---|---|---|---|---|---|
| groupby_mean | 394.9 / 238.1 / 223.3 | 238.1 | 2000000 | 50342651 | 34669 |
| sort_values | 470.0 / 563.1 / 433.3 | 470.0 | 2000000 | 50342651 | 34669 |
| value_counts | 7.6 / 4.9 / 6.1 | 6.1 | 2000000 | 50342651 | 34669 |
| filter_sum | 24.4 / 26.6 / 26.1 | 26.1 | 2000000 | 50342651 | 34669 |
| to_csv | 3083.6 / 3721.4 | 3402.5 | 2000000 | 50342651 | 34669 |
| read_csv | 922.7 / 842.7 | 882.7 | 2000000 | 50342651 | 34669 |
| matplotlib_png | 169.3 / 120.1 / 117.5 / 101.8 / 129.7 | 120.1 | 2000000 | 50342651 | 34669 |
CSV のサイズは50342651バイトなので、10進表記では50.3MB ほどになる。3402.5ms で書いた計算だと、書き出しは約14.8MB/s。読み込みは882.7ms なので約57.0MB/s まで上がった。
この差を見ると、ディスクに順番に書く速度そのものが主因とは考えにくい気がする。実際に SSD が遅いというより、数値や文字列を CSV のテキストへ変換し、区切り文字と改行を混ぜて吐き出す CPU 側の仕事が支配的に見える。ディスクの書き込み速度から見ても、14.8MB/s は低すぎる印象だった。
集計より文字列化が重い
value_counts の6.1ms はかなり軽い。2000000行を見ているのに、ほとんど待たない。フィルタして合計する処理も26.1ms なので、ちょっとした前処理を足しても CSV 書き出しの3402.5ms には届きにくい。
groupby_mean は初回だけ394.9ms で、2回目以降は238.1ms と223.3ms だった。キャッシュや内部のウォームアップの影響はありそうだが、それでも CSV の領域とは桁が違う。sort_values は470.0ms、563.1ms、433.3ms と揺れている。並べ替えは重い部類だと思っていたが、今回の範囲では to_csv の前では軽く見える。
個人的には、CSV を「遅いけど無難な中間形式」と見ていたが、今回の数字だと、中間データを何度も CSV で持ち回る設計は、そのたびに秒単位の税金を払うことになる(小さい処理をいくら磨いても、最後の書き出しで待つ構図になる)。処理本体を速くしても、最後に毎回 CSV 化していれば体感は変わりにくい。
read_csv も軽くはない。882.7ms なら、数回なら気にならないが、バッチで何十回も読むと効いてくる。ただ、今回いちばん目立ったのは書き出し側だった。3402.5ms は長く、読み込みより3.9倍かかっていることも合わせると、最後に CSV を吐く処理を安易に増やしたくない(ここが今回のボトルネックに見える)。
parquet は試したが失敗した
parquet も測るつもりだったが、ここは失敗として残しておきたい。ここで CSV より小さく速い結果を期待していたのだが、手元の環境では pyarrow も fastparquet も入っておらず、ImportError で止まってしまった。
エラー内容は、pandas が parquet 用の利用可能な engine を見つけられない、というものだ。pyarrow の import に失敗し、fastparquet も必要だと表示されている。つまり今回の表には to_parquet の時間も parquet ファイルのサイズも載せられなかった。
この部分はやり直し候補で、pip で pyarrow を入れれば測れる可能性は高いが、今回は依存関係を足す前の素の環境でどこまで行けるかを見たかった(配布先で同じように詰まるかもしれない)。だから parquet は「速かった」でも「遅かった」でもなく、失敗として記録しておく。
この失敗は地味に大事で、CSV から逃げたいと思っても、別形式は依存パッケージ込みで初めて選択肢になるのだと分かった(ローカルの venv だけで完結する話ではない)。CSV から逃げたいと思っても、parquet は標準の pandas だけで必ず使える形式とは限らなかった。配布するスクリプトや再現手順では、依存パッケージを明示しないと相手の環境で動かなかった、という話になりやすい(自分だけの venv では気づきにくいところ)。
matplotlib の保存も積み上がる
matplotlib の PNG 出力は中央値120.1ms で、CSV 書き出しほどではないにしても、グラフを大量に吐くバッチなら無視できない待ち時間になる(画像サイズだけでは軽さを判断できない)。ファイルサイズは34669バイトなので、出力される画像そのものは大きくない。にもかかわらず、描画して保存する処理には100ms 台がかかる。
1枚だけなら気にならない。実際に手元でグラフを1本作る用途なら、120.1ms は待ち時間としてほぼ見えない。一方で、バッチでグラフを大量に生成する処理では話が変わる。CSV の3402.5ms ほどではないが、PNG 保存も無料ではない。
ここでも、ファイルサイズだけ見て判断すると間違える。34669バイトしかないから軽い、と言いたくなるが、実際に測った時間は120.1ms だった。画像のエンコード、フォント、描画バックエンドなど、単純なバイト数とは別の処理が入っている。
中間データを CSV で持ち回らない
今回の運用上の結論はかなり単純。中間データを CSV で何度も保存して読み直すのをやめる方向で考える、という話。
最終成果物として人に渡す CSV は残してよい。Excel で開けて、差分確認もしやすい形式である。けれど、Python の処理同士をつなぐためだけの一時ファイルなら、CSV にする理由はあまり残りにくい。少なくとも今回の測定では、groupby や sort_values を削るより、CSV の回数を減らすほうが効く。
次に試すなら、pyarrow を入れた状態で parquet を測り直してみる。CSV の50.3MB に対してどれだけ小さくなるか、書き出しが3402.5ms からどこまで下がるかも確認したい。pandas 3.0.0rc2 ではなく安定版でも同じ傾向になるかは、別の回で確認する。
ARM64 Windows だから pandas が遅い、という感触はなかった。むしろ DataFrame の処理は普通に速く、CSV の文字列化で詰まった。そこに気づけたので、次にバッチを書くときは、計算式より先にファイル形式を疑う。