**Pandas with Rows(2022)**

2025/12/26 17:52

**Pandas with Rows(2022)**

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

(欠落した詳細と明確化を統合)**


要約

この記事では、1億2000万件のレコードから米国で平均国内フライト遅延が最大の5つの空港を見つけるためのいくつかの手法をベンチマークしています。

  • ベースライン: すべての年次CSVに対して
    pandas.concat
    を使用すると MemoryError が発生します。
  • 純Pythonストリーミング: 空港ごとのカウント/遅延をメモリ内で保持(約1 MB)し、実行時間は約7分です。
  • PyPy: 同じコードを PyPy で実行すると RAM が約40 MB使用され、完了までに約4 m 40 s(CPython の約⅔)かかります。
  • 最適化されたpandas:
    Origin
    Year
    Month
    DayofMonth
    CRSDepTime
    DepTime
    だけを読み込み、それらを効率的な dtype(
    category
    uint16/uint8
    )にキャストします。これで実行時間が約2 m 45 sに短縮され、結合時のピークは8.1 GBです。CSV のロードに 80 % 以上が費やされています。
  • Pandas + PyArrow エンジン:
    engine='pyarrow'
    に切り替えると実行時間が約1 m 10 s(元の約42 %)に短縮され、ピークメモリは8.1 GBを維持します。マルチスレッドは役立ちますが、ディスク I/O がボトルネックです。
  • 直接 PyArrow API: 各年次 CSV を Arrow テーブル(
    convert_options
    )に読み込み、
    pyarrow.concat_tables
    で結合し、pandas に変換すると約50 sの実行時間になり、ピーク RAM は7.5 GBです(pandas/PyArrow のピークの93 %)。
  • 年ごとのアプローチ: 年を1年ずつ処理し、空港ごとの合計/カウントを集約してから年全体で縮減すると、実行時間は約37 s、ピーク RAM はわずか900 MB(完全ロード法の12 %)です。
  • 並列 pandas(8 ワーカー): 各 CSV を並列に読み込む multiprocessing プールを使用すると実行時間が約53 s(単一コア pandas の約1/3)まで短縮されますが、ディスク I/O に制限されます。プロセスごとのピーク RAM は約400 MBです。

データセットはハーバード大学の「Data Expo 2009: Airline on time data」(1987‑2008年)から取得され、22 個の CSV ファイルで約13 GBに圧縮されています。テストは単一 Linux マシン(i7‑8550U、16 GB LPDDR3 RAM、NVMe SSD、スワップなし)上で Arch Linux + KDE Plasma 環境で実行されました。

結論: メモリ制限はストリーミングや年ごとの処理によって回避できます。パフォーマンス向上は PyPy、PyArrow エンジン、または直接 Arrow API によって得られます。並列化はわずかなスピードアップを提供しますが、最終的にはディスクスループットに制限されます。これらの知見は、大規模航空データセットを扱うデータサイエンティストや航空会社/規制当局が遅延パターンを分析する際に役立ちます。


本文

問題

米国内線で平均(算術平均)遅延が最大のアメリカ空港トップ 5を見つけたい。


データ

  • データセット:Harvard Dataverse の Data Expo 2009 – Airline on‑time data
    • 米国内のすべて商業フライト(到着・出発)の詳細。1987年10月〜2008年4月。
    • 約1億2,000万件を22個のCSVファイル(各年1ファイル)+無視する4個の補助CSVで構成。
    • 圧縮時サイズ ≈ 13 GB。

環境

コンポーネントスペック
CPUIntel® Core™ i7‑8550U @ 1.80 GHz
RAM16 GB LPDDR3、スワップ無し
ディスクNVMe TOSHIBA 512 GB (ext4)
OSLinux 5.19.9 (Arch)、KDE Plasma、Konsole 1セッション
ソフトウェアCPython 3.10.6、PyPy 7.3.9(Python 3.9)、pandas 1.4.4、PyArrow 9.0.0

単純なアプローチ

import pandas as pd
df = pd.concat(
    pd.read_csv(f'{year}.csv') for year in range(1987, 2009)
)

すべてのデータをメモリに読み込もうとすると

MemoryError
(またはカーネルが終了)になる。
RAM に収まらないため。


純Pythonでの解決策

CSV をストリームし、空港ごとの集計だけを保持する。

import csv, datetime, heapq, operator

USE_COLS = ('Origin', 'Year', 'Month', 'DayofMonth',
            'CRSDepTime', 'DepTime')

airports = {}

for year in range(1987, 2009):
    with open(f'../data/{year}.csv', errors='ignore') as f:
        reader = csv.reader(f)
        header = {name: pos for pos, name
                  in enumerate(next(reader))
                  if name in USE_COLS}

        for row in reader:
            if (row[header['CRSDepTime']] == 'NA' or
                row[header['DepTime']]     == 'NA'):
                continue

            y, m, d = (int(row[header['Year']]),
                       int(row[header['Month']]),
                       int(row[header['DayofMonth']]))
            try:
                sched = datetime.datetime(y, m, d,
                                          int(row[header['CRSDepTime']][:-2] or '0'),
                                          int(row[header['CRSDepTime']][-2:]))
                act   = datetime.datetime(y, m, d,
                                          int(row[header['DepTime']][:-2] or '0'),
                                          int(row[header['DepTime']][-2:]))
            except ValueError:
                continue

            delay = (act - sched).total_seconds() / 3600.0
            if delay < -2.0:
                delay = 24.0 - delay

            key = row[header['Origin']]
            airports.setdefault(key, [0, 0.0])
            airports[key][0] += 1
            airports[key][1] += delay

# 平均遅延でトップ5
top5 = dict(heapq.nlargest(
    5,
    ((a, total / count) for a, (count, total) in airports.items()),
    operator.itemgetter(1)
))
print(top5)
  • メモリ:約 1 MB
  • 時間:≈ 7 分

PyPy

同じスクリプトを PyPy で実行すると、メモリは ≈ 40 MB、終了時間は ≈ 4 m 40 s(CPython の約 ⅔)になる。


pandas – メモリ効率の良いアプローチ

import pandas as pd

LOAD_COLS = ('Origin', 'Year', 'Month', 'DayofMonth',
             'CRSDepTime', 'DepTime')

df = pd.concat(
    pd.read_csv(f'../data/{fname}.csv',
                usecols=LOAD_COLS,
                encoding_errors='ignore',
                dtype={'Origin': 'category',
                       'Year': 'uint16',
                       'Month': 'uint8',
                       'DayofMonth': 'uint8',
                       'CRSDepTime': 'uint16',
                       'DepTime': 'UInt16'})
    for fname in range(1987, 2009),
    ignore_index=True
)

date = pd.to_datetime(df[['Year', 'Month', 'DayofMonth']].rename(columns={'DayofMonth':'Day'}))
df['scheduled_dep'] = date + pd.to_timedelta(
        (df['CRSDepTime']//100)*60 + (df['CRSDepTime']%100),
        unit='minutes')
df['actual_dep']   = date + pd.to_timedelta(
        (df['DepTime']//100)*60 + (df['DepTime']%100),
        unit='minutes')

df = df[['Origin', 'scheduled_dep', 'actual_dep']]
df['delay'] = (df['actual_dep'] - df['scheduled_dep']).dt.total_seconds() / 3600
df['delay'] = df['delay'].where(df['delay'] > -2, 24 - df['delay'])

print(df.groupby('Origin')['delay']
        .mean()
        .sort_values(ascending=False)
        .head(5))
  • ピークメモリ:≈ 8.1 GB(
    pd.concat
    時)
  • 時間:約 2 分 45 秒

pandas + PyArrow エンジン

df = pd.concat(
    pd.read_csv(f'../data/{fname}.csv',
                usecols=LOAD_COLS,
                encoding_errors='ignore',
                dtype={'Origin': 'category',
                       'Year': 'uint16',
                       'Month': 'uint8',
                       'DayofMonth': 'uint8',
                       'CRSDepTime': 'uint16',
                       'DepTime': 'UInt16'},
                engine='pyarrow')
    for fname in range(1987, 2009),
    ignore_index=True
)
  • ピークメモリ:同上
  • 時間:≈ 1 分 10 秒(通常の pandas 実行時間の約 42%)

直接 PyArrow

import pyarrow as pa, pyarrow.csv as pacsv

COLUMN_TYPES = {
    'Origin': pa.dictionary(pa.int32(), pa.string()),
    'Year': pa.uint16(),
    'Month': pa.uint8(),
    'DayofMonth': pa.uint8(),
    'CRSDepTime': pa.uint16(),
    'DepTime': pa.uint16()
}

tables = []
for year in range(1987, 2009):
    tables.append(
        pacsv.read_csv(f'../data/{year}.csv',
                       convert_options=pa.csv.ConvertOptions(
                           include_columns=COLUMN_TYPES,
                           column_types=COLUMN_TYPES))
    )
df = pa.concat_tables(tables).to_pandas()
  • ピークメモリ:≈ 7.5 GB
  • 時間:約 50 秒

PyArrow 年別(逐次集計)

import functools, pyarrow as pa, pyarrow.csv as pacsv, pandas as pd

COLUMN_TYPES = {...}  # 上と同じ

results = []
for year in range(1987, 2009):
    df = pacsv.read_csv(
        f'../data/{year}.csv',
        convert_options=pa.csv.ConvertOptions(
            include_columns=COLUMN_TYPES,
            column_types=COLUMN_TYPES)
    ).to_pandas()

    date = pd.to_datetime(df[['Year', 'Month', 'DayofMonth']].rename(columns={'DayofMonth':'Day'}))
    df['scheduled_dep'] = date + pd.to_timedelta((df['CRSDepTime']//100)*60 + (df['CRSDepTime']%100),
                                                 unit='minutes')
    df['actual_dep']   = date + pd.to_timedelta((df['DepTime']//100)*60 + (df['DepTime']%100),
                                                 unit='minutes')

    df = df[['Origin', 'scheduled_dep', 'actual_dep']]
    df['delay'] = (df['actual_dep'] - df['scheduled_dep']).dt.total_seconds() / 3600
    df['delay'] = df['delay'].where(df['delay'] > -2, 24 - df['delay'])

    results.append(df.groupby('Origin')['delay'].agg(['sum', 'count']))

# 年間を統合
df = functools.reduce(lambda x, y: x.add(y, fill_value=0), results)
df['mean'] = df['sum'] / df['count']
print(df['mean'].sort_values(ascending=False).head(5))
  • ピークメモリ:≈ 900 MB
  • 時間:約 37 秒

pandas 年別+マルチプロセッシング

import functools, multiprocessing as mp, pandas as pd

LOAD_COLS = ('Origin', 'Year', 'Month', 'DayofMonth',
             'CRSDepTime', 'DepTime')

def read_one_csv(year):
    df = pd.read_csv(f'../data/{year}.csv',
                     engine='c',
                     usecols=LOAD_COLS,
                     encoding_errors='ignore',
                     dtype={'Origin': 'category',
                            'Year': 'uint16',
                            'Month': 'uint8',
                            'DayofMonth': 'uint8',
                            'CRSDepTime': 'uint16',
                            'DepTime': 'UInt16'})

    date = pd.to_datetime(df[['Year', 'Month', 'DayofMonth']].rename(columns={'DayofMonth':'Day'}))
    df['scheduled_dep'] = date + pd.to_timedelta((df['CRSDepTime']//100)*60 +
                                                 (df['CRSDepTime']%100),
                                                 unit='minutes')
    df['actual_dep']   = date + pd.to_timedelta((df['DepTime']//100)*60 +
                                                 (df['DepTime']%100),
                                                 unit='minutes')

    df = df[['Origin', 'scheduled_dep', 'actual_dep']]
    df['delay'] = (df['actual_dep'] - df['scheduled_dep']).dt.total_seconds() / 3600
    df['delay'] = df['delay'].where(df['delay'] > -2, 24 - df['delay'])

    return df.groupby('Origin')['delay'].agg(['sum', 'count'])

pool = mp.Pool()
df = functools.reduce(lambda x, y: x.add(y, fill_value=0),
                      pool.map(read_one_csv, range(1987, 2009)))
df['mean'] = df['sum'] / df['count']
print(df['mean'].sort_values(ascending=False).head(5))
  • プロセスあたりピークメモリ:≈ 400 MB
  • 総時間:約 53 秒(通常の pandas 実行時間の約 ⅓)
  • PyArrow に対する速度向上はディスク I/O がボトルネックになるため、ワーカーを増やしすぎると逆に遅くなる。

要点まとめ

アプローチピーク RAM時間
純Python~1 MB7 min
PyPy (スクリプト)~40 MB4 m 40 s
pandas (C エンジン、全データ)8.1 GB2 min 45 秒
pandas + PyArrow エンジン8.1 GB1 分 10 秒
Direct PyArrow7.5 GB50 s
PyArrow 年別(逐次集計)900 MB37 s
pandas 年別+マルチプロセッシング~400 MB/プロセス53 秒

学び

  • ストリーミング/逐次集計はメモリを低く保ち、時に一括読み込みより高速になる。
  • PyArrow のマルチスレッド CSV リーダーはデフォルト pandas エンジンより大幅に速い。
  • マルチプロセッシングは CPU がボトルネックのときだけ有効で、I/O が支配的なら効果が薄い。

さらに読む

  • High Performance Python – Python コードを高速化する実践ガイド。
  • 私の講演「Demystifying pandas internals」(PyData London 2018)。
  • Telegram チャンネル https://t.me/datapythonista でクイックノートやミームを共有しています。

同じ日のほかのニュース

一覧に戻る →

2025/12/30 6:46

USPS(米国郵便公社)が切手印日付システムの変更を発表しました。

## Japanese Translation: > **概要:** > USPSは最終規則(FR Doc. 2025‑20740)を発行し、国内郵便マニュアルに「セクション 608.11 —『切手印と郵便保有』」を追加しました。この規則では、切手印の定義が正式に示され、該当する印記がリストアップされています。切手印は印付け日でUSPSがその物件を保有していることを確認しますが、必ずしもアイテムの最初の受理日と同一ではありません。USPSは通常業務で全ての郵便に切手印を貼らないため、切手印が欠落していても、その物件が未処理だったとは限りません。機械による自動切手印は、施設内で最初に行われた自動処理操作の日付(「date of the first automated processing operation」)を表示し、投函日ではなく、地域輸送最適化(RTO)や路線ベースのサービス基準により受理日より遅くなることがあります。切手印は小売ユニットからの輸送後やカレンダー日がまたがる場合に付けられることが多いため、郵送日を示す信頼できる指標ではありません。同一日の切手印を確保するには、小売窓口で手動(ローカル)切手印を依頼できます。小売窓口で料金を支払うと「Postage Validation Imprint(PVI)」が付与され、受理日が記録されます。また、郵便証明書、登録メール、または認定メールは提示日を裏付ける領収書として機能します。この規則の影響は税務申告において重要です。IRC §7502 は、文書が期限までに物理的に届けられなかった場合に、提出の適時性を判断する際に切手印の日付を使用しています。

2025/12/30 1:07

**Zig における静的割り当て** Zig のコンパイル時メモリ管理を使えば、実行時ではなくコンパイル時にストレージを確保できます。データ構造のサイズが事前に分かっている場合やヒープ割り当てを避けたいときに便利です。 ### 重要概念 - **コンパイル時定数** `const` や `comptime` の値を使い、コンパイラがコンパイル中に評価できるサイズを記述します。 - **固定長配列** リテラルサイズで配列を宣言します。 ```zig const buf = [_]u8{0} ** 128; // 128 バイト、すべてゼロ初期化 ``` - **静的フィールドを持つ構造体** 固定長配列やその他コンパイル時に決まる型を含む構造体を定義します。 ### 例 ```zig const std = @import("std"); // 静的サイズのバッファを持つ構造体 pub const Message = struct { id: u32, payload: [256]u8, // 256 バイト、コンパイル時に確保 }; // 静的割り当てを使う関数 fn process(msg: *Message) void { // ヒープ割り当ては不要;msg はスタック上またはグローバルに存在 std.debug.print("ID: {d}\n", .{msg.id}); } pub fn main() !void { var msg = Message{ .id = 42, .payload = [_]u8{0} ** 256, // すべてのバイトをゼロで初期化 }; process(&msg); } ``` ### 利点 - **決定的なメモリ使用量** – サイズはコンパイル時に分かる - **実行時割り当てオーバーヘッドがゼロ** – ヒープアロケータ呼び出しなし - **安全性** – コンパイラが境界と寿命を検証できる ### 使うべき場面 - 固定長バッファ(例:ネットワークパケット、ファイルヘッダー) - 短時間しか存続しない小規模補助データ構造 - 性能や決定的な動作が重要な状況 --- コンパイル時定数・固定配列・構造体定義を活用することで、Zig は最小限のボイラープレートで最大の安全性を保ちつつメモリを静的に割り当てることができます。

## Japanese Translation: > **概要:** > このプロジェクトは、Zigで書かれた軽量Redis互換のキー/バリューサーバー「kv」を構築し、最小限のコマンドセットで本番環境に適した設計を目指しています。コアデザインでは起動時にすべてのメモリを確保することで、実行中にダイナミックヒープを使用せず、レイテンシスパイクやユース・アフター・フリー(use‑after‑free)バグを回避します。接続は`io_uring`で非同期に処理され、システムは3つのプール(Connection、受信バッファプール、送信バッファプール)を事前確保し、デフォルトでは約1000件までの同時接続数をサポートします。各接続は設定パラメータから派生した固定サイズの受信/送信バッファを使用します。 > コマンド解析はRedisのRESPプロトコルのサブセットに従い、Zigの`std.heap.FixedBufferAllocator`を用いてゼロコピーで解析し、各リクエスト後にアロケータをリセットします。バッファサイズは`list_length_max`と`val_size_max`に依存します。 > ストレージは未管理型の`StringHashMapUnmanaged(Value)`を使用し、初期化時に`ensureTotalCapacity`で容量を確保します。キーと値は共有`ByteArrayPool`に格納され、マップはポインタのみを保持します。削除操作では墓石(tombstone)が残り、墓石数が増えると再ハッシュが必要になる場合があります。 > 設定構造体(`Config`)は `connections_max`、`key_count`、`key_size_max`、`val_size_max`、`list_length_max` などのフィールドを公開し、派生アロケーションで接続ごとのバッファサイズを決定します。デフォルト設定(総計約748 MB、2048エントリ)では `val_size_max` または `list_length_max` を倍増すると、割り当て量が約2.8 GBに上昇する可能性があります。 > 今後の作業としては、カスタム静的コンテキストマップ実装の改善、より良いメモリ利用を実現する代替アロケータの探索、境界検査(fuzz)テストの追加による限界確認、および墓石再ハッシュ処理への対応が挙げられます。

2025/12/27 20:30

**フレームグラフ 対 ツリーマップ 対 サンバースト(2017)**

## Japanese Translation: **概要:** Flame グラフ(SVG)はディスク使用量を高レベルで明確に示します。たとえば、Linux 4.9‑rc5 では `drivers` ディレクトリが全容量の50%以上を占め、`drivers/net` サブディレクトリは約15%です。Tree マップ(macOS の GrandPerspective、Linux の Baobab)は非常に大きなファイルを素早く検出できますが、高レベルのラベルが欠けています;Baobab のツリー表示では各ディレクトリの横にミニバーグラフが表示されます。Sunburst(Baobab の極座標図)は視覚的に印象的ですが、角度で大きさを判断するため長さや面積よりも誤解しやすいです。他のツール―`ncdu` の ASCII バーと `du -hs * | sort -hr` ―はテキストベースで迅速なサマリーを提供しますが、同時に一階層のみ表示されます。 提案されたユーティリティは、これら三つの可視化(Flame グラフ(デフォルト)、Tree マップ、Sunburst)すべてを組み合わせるものです。Flame グラフは読みやすさ・印刷性・最小スペース使用量が優れているため、多数のサンプルファイルシステムでテストした後にデフォルトとして採用されます。このアプローチは、ディスク使用量を簡潔かつ印刷可能なスナップショットとして提供し、ユーザーや開発者がスペースを占有する項目をより効率的に検出できるよう支援します。アイデアは ACMQ の「The Flame Graph」記事と「A Tour through the Visualization Zoo」に引用された既存の研究に基づいています。 **反映された主なポイント:** flame グラフの高レベルビュー、Tree マップの大きなファイルを素早く検出できるがラベルが欠けている点、Sunburst の視覚的魅力とサイズ認識の問題、他ツールの制限、および提案ツールの三つのビュー(デフォルトは flame グラフ)と引用元への参照。

**Pandas with Rows(2022)** | そっか~ニュース