【Python】大きなCSVを分割して正しく集計する

PythonのTopに戻る

大きなCSVの平均は、チャンクごとの平均を単純平均せず、合計と有効件数を足し合わせて最後に割る。chunksizeは一度に読む行数を制限するが、集計の正しさまで保証するものではない。欠測や最終チャンクの長さが違っても同じ定義の平均になるよう、集約可能な量を保持する。

最小例で確かめる

以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。

from io import StringIO
import numpy as np
import pandas as pd
csv_text = "value\n1\n3\n\"\"\n9\n\"\"\n"
total = 0.
count = 0
rows = 0
chunk_means = []
with pd.read_csv(StringIO(csv_text), usecols=["value"],
                 dtype={"value": "float64"}, chunksize=2) as reader:
    for chunk in reader:
        values = chunk["value"]
        if np.isinf(values.to_numpy()).any():
            raise ValueError("infinite value is not allowed")
        n = int(values.count())
        total += float(values.sum())
        count += n
        rows += len(chunk)
        if n:
            chunk_means.append(float(values.mean()))
mean = total / count if count else float("nan")
wrong = float(np.mean(chunk_means))
print("rows / valid:", rows, count)
print("sum / mean:", total, round(mean, 6))
print("wrong mean of means:", wrong)
# 小さい例だけで全件読み込みと照合する。
whole = pd.read_csv(StringIO(csv_text), dtype={"value": "float64"})
np.testing.assert_allclose(mean, whole["value"].mean())
assert rows == 5 and count == 3 and total == 13.
assert not np.isclose(mean, wrong)

実行結果

rows / valid: 5 3
sum / mean: 13.0 4.333333
wrong mean of means: 5.5

平均の平均がずれる理由

最初のチャンクには1と3があり、平均は2、有効件数は2である。次のチャンクは欠測と9なので、平均は9、有効件数は1になる。この二つの平均を同じ重みで平均すると5.5だが、全体の3値の平均は13/3で約4.333333である。各チャンクを同じ一票として扱ったことがずれの原因になる。

チャンクサイズが同じでも、欠測数が違えば有効件数は同じにならない。最後のチャンクだけ短い場合もある。そこで各チャンクから合計と有効件数を取り出し、それぞれ足し合わせる。平均を保存する場合でも件数で重み付けが必要なので、最初から合計と件数を保持する方が分かりやすい。

全欠測チャンクと空の入力を考える

この例の最後のチャンクは欠測だけである。sumの結果が0でも、countは0なので平均に新しい観測が加わったとは扱わない。最終的なcountが0なら平均をNaNにすることで、観測が全くない状況をゼロ平均と区別している。ヘッダーだけの入力など、データ件数が少ない場合も考えておこう。

空欄の行をCSVへ置く場合、単なる空行はread_csvの既定では読み飛ばされる。例では引用符付きの空セルを使い、測定行が存在するが値は欠測という状況を明示した。実際のファイルでも、空行と空セルをどう解釈するかにより行数の意味が変わる。読み込み規則と集計規則を一緒に確認する必要がある。

メモリを節約する条件

chunksizeを指定するとTextFileReaderから部分表を順に得られる。各chunkをリストへ保存し続けたり、最後にすべてconcatしたりすると、全件を保持するためのメモリが必要になる。この例では固定個数の集計値だけを残している。chunk_meansは誤りを示す小例用であり、大規模な実処理では保存しなくてよい。

usecolsで必要な列に絞り、dtypeを指定するとチャンク間の型の違いを減らせる。あるチャンクだけ文字列が混ざる場合には、無言で除外するより変換失敗を別に記録する設計が望ましい。例では無限大を認めない方針として明示的に例外を出した。NaNを除く平均と有限値だけの平均は同じ定義とは限らない。

何でも分割集計できるわけではない

合計、件数、最小値、最大値などは比較的まとめやすいが、中央値や分位点はチャンクごとの値を同じ方法で集約しても全体の値にならない。分散にも専用の合成方法が必要である。処理を分割する前に、保持する情報だけで最終結果を再構成できるか確認しよう。

群別集計では群ごとの合計と件数を保持すればよいが、群数そのものが大きいと集計表がメモリを使う。浮動小数点の和は分割順で末尾がわずかに変わる場合もあるため、小さい入力で全件処理と許容誤差付きで比較する。掲載例の全件読み込みはこの照合のためだけに行っており、大きな実ファイルで必須の手順ではない。

動作確認環境と参考資料

Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。

関連するTips

PythonのTopに戻る