ターミナルだけで完結するデータ分析環境の構築検討:Codex CLI + awk, q, duckdb
要約(TL;DR)
-
awk:単一CSVの軽量集計・整形向き。依存ほぼゼロでどこでも動く。JOIN/複雑統計は苦手。
-
q:CSVをSQLで叩ける。単一ファイルは
stdin + FROM -
が安定。複数JOINはファイル名のクォートが罠。 -
DuckDB:CSV/Parquet/JSONをRDB級の表現力で爆速処理。JOIN/ウィンドウ/型推定/関数が揃ってる。
-
運用指針(Codex 連携)
-
単一ファイル(軽整形・一次統計) → awk
- 目安: 行数 < 5万・列数 < 50、JOIN/日時関数なし
-
単一ファイル(SQLで書きたい) → q(
stdin + FROM -
)- 目安: WHERE/GROUP BY/ORDER BY を素直にSQLで
-
複数ファイルJOIN / 複雑集計 / 型推定・日時関数あり / 100MB超 → DuckDB
- 基本は DuckDB 一択。read_csv_auto + ウィンドウ/集計で完結
-
どれにも当てはまらない軽い用途 → awk
-
(個人的には、
duckdb
+awk
のみで十分だと思う)
-
ツールの特徴と優位性
awk
-
強み:軽い・標準搭載・行処理が速い。一次統計、フィルタ、整形が得意。
-
苦手:JOIN/複雑集計/日時関数。
-
例(基本統計量・ヘッダ1行スキップ):
awk -F, 'NR>1{n++; s+=$2; s2+=$2*$2; if(n==1){min=$2;max=$2} else{min=($2<min)?$2:min; max=($2>max)?$2:max}} END{m=s/n; sd=sqrt(s2/n - m*m); printf "n=%d mean=%.4f sd=%.4f min=%.3f max=%.3f\n", n,m,sd,min,max}' file.csv
q(text-as-data)
-
強み:SQLで直感的に。
WHERE/GROUP/ORDER
が気持ちよく書ける。 -
落とし穴:ファイル名・列名のクォート
-
例:
q -H -d, "SELECT COUNT(*) n, AVG(CAST(eruptions AS REAL)) mean FROM - " < faithful.csv
DuckDB
-
強み:JOIN/ウィンドウ/複雑統計/型推定が楽。
read_csv_auto
が賢い。Parquet/JSONもOK。 -
例(CSVの一次統計):
duckdb -c "SELECT COUNT(*) n, AVG(eruptions) mean_e, STDDEV_SAMP(eruptions) sd_e, MIN(eruptions) min_e, MAX(eruptions) max_e, AVG(waiting) mean_w, STDDEV_SAMP(waiting) sd_w, MIN(waiting) min_w, MAX(waiting) max_w FROM read_csv_auto('faithful.csv')"
ベンチマーク設計
タスク & データセット
-
指示: 「2025-04-01の統計値を算出して」
- 3つのテーブルを Date で JOIN
- 2025-04-01 に絞り込み
- 各品質(QualityA/B/C)ごとに件数/平均/合計などを算出, となる想定
-
データ:
-
production_101.csv
: Date, Line101, QualityA, QualityB, QualityC, Amount -
production_121.csv
: Date, Line121, QualityA, QualityB, QualityC, Amount -
weather.csv
: Date, Temperature, Humidity, Wind Speed, Wind Direction, Weather
-
実験条件
-
パータン:
awk
/q
/DuckDB
の単独3 + 順列コンビ4 = 計7パターン(手順は AGENTS.md に定義) -
計測:
-
実行時間 / 常駐メモリ:
/usr/bin/time -f '%E %M' <command>
-
%E
: 経過時間 (min:sec) -
%M
: 最大常駐集合サイズ (KB)
-
-
トークン消費: Codex 実行ログの tokens used を採用(モデル別に記録)
-
パターン | 中央値(実行時間) | ベスト | ワースト | 中央値(最大メモリKB) | 中央値(消費トークン) |
---|---|---|---|---|---|
duckdbのみ | 2:24.76 | 1:55.38 | 2:54.14 | 118,572 | 7,285 |
awkのみ | 4:28.99 | 2:47.40 | 6:10.57 | 118,234 | 7,366 |
awk → q → DuckDB | 4:15.94 | 2:12.97 | 6:18.90 | 117,988 | 13,408 |
DuckDB → q → awk | 5:50.99 | 3:57.87 | 7:44.10 | 118,486 | 14,676 |
q(stdin) → awk → DuckDB | 8:17.32 | 6:05.04 | 10:29.60 | 118,316 | 10,594 |
qのみ | 9:15.41 | 7:43.15 | 10:47.67 | 118,374 | 11,252 |
q → DuckDB → awk | 10:55.73 | 10:08.53 | 11:42.93 | 118,290 | 14,838 |
所感(このデータセット・手順に限る):
-
速度・トークンともに DuckDB 単独が最良(中央値 2:24.76 / 7,285 tok)。
-
ワークフローを跨ぐほど(q→DuckDB→awk 等)遅く・トークンも増える=Orchestration コストが無視できない。
-
awk 単独は軽作業なら悪くないが、JOIN/統計を入れた途端に表現力の差で手数が増える。
-
メモリは計測系のオーバーヘッドでほぼ横並び(この規模では差が出にくい)。
AGENTS.md 全文
- awk のみ
## データアクセス & 分析ルール (S1: awkのみ)
- 常に **awk** を使う。q や DuckDB は利用しない。
- 最初の行は列名なので `head -n1 file.csv` で確認してから処理を書く。
- 基本例:
```bash
awk -F, 'NR>1{n++; s+=$2} END{print "mean:",s/n}' file.csv
```
- q のみ
## データアクセス & 分析ルール (S2: qのみ)
- 常に **q** を使う。awk や DuckDB は利用しない。
- 列名は `head -n1 file.csv` で確認して SQL を組み立てる。
- 単一ファイルは必ず stdin + `FROM -` を徹底:
```bash
q -H -d, "SELECT * FROM - LIMIT 5" < file.csv
```
- 複数ファイルを扱う場合は `-f` とダブルクォート付きファイル名を利用:
```bash
q -H -d, -f a.csv -f b.csv -- 'SELECT * FROM "a.csv" JOIN "b.csv" USING(id)'
```
- DuckDB のみ
## データアクセス & 分析ルール (S3: DuckDBのみ)
- 常に **DuckDB** を使う。awk や q は利用しない。
- 列名は `head -n1 file.csv` で確認してクエリを作成。
- 単一・複数を問わず `read_csv_auto` を利用:
```sql
SELECT COUNT(*), AVG(col1)
FROM read_csv_auto('file.csv', HEADER=TRUE);
```
- 複数ファイル JOIN も DuckDB で記述する。
- q → DuckDB → awk
## データアクセス & 分析ルール (C1: q → DuckDB → awk)
- 単一ファイル: **q (stdin + FROM -)**。
- 複数ファイルや複雑な集計: **DuckDB**。
- どちらも使えない場合: **awk**。
- 列名は必ず `head -n1 file.csv` で確認してクエリを生成。
- awk → q → DuckDB
## データアクセス & 分析ルール (C2: awk → q → DuckDB)
- 簡単な集計や整形: **awk**。
- SQL 的な条件や集計: **q (stdin + FROM -)**。
- JOIN や高度な分析: **DuckDB**。
- 列名確認は `head -n1` を必須とする。
- DuckDB → q → awk
## データアクセス & 分析ルール (C3: DuckDB → q → awk)
- 既定は **DuckDB** (`read_csv_auto`)。
- 軽い処理なら **q (stdin)**。
- DuckDB/q が無ければ **awk**。
- クエリ作成前に必ず `head -n1` で列名を確認。
- q(stdin) → awk → DuckDB
## データアクセス & 分析ルール (C4: q(stdin) → awk → DuckDB)
- 単一ファイル: **q (stdin + FROM -)** を必ず使用。
- q が不在/失敗した場合: **awk**。
- JOIN や複雑集計: **DuckDB**。
- 列名は `head -n1 file.csv` で確認してから処理。
Tips & Tricks
-
CSVのクォート対応(awk):
-v FPAT='([^,]+)|(\"([^\"]|\"\")*\")'
でカンマ入りフィールドに強くなる。出力は-v OFS=,
で安定化。 -
q の鉄則:単一ファイルは
stdin + FROM -
。複数は-f
とダブルクォート付きファイル名、スペースを含む列名もダブルクォート。 -
DuckDB のお作法:
read_csv_auto('file.csv')
は賢いが、日時文字列はSTRPTIME()
で明示的に型変換すると後段が安定。重い時はPRAGMA threads=auto; PRAGMA memory_limit='4GB';
。 -
BOM 除去:
sed -i '1s/^\xEF\xBB\xBF//' *.csv
-
失敗時リトライ禁止:同一条件で2回連続失敗したら、状況整理→仮説構築→手を打つ(無目的リトライはしない)。
まとめ(結論)
-
まず DuckDB で完結できないかを考える
-
軽い単発は awk
-
ワークフロー分割はコスト(遅く・高トークンになりがち)→ 単独完結の設計を優先。
-
Codex には明確な分岐規則を実装し、失敗2連続で状況整理→仮説構築に即切替
最後に、AGENTS.mdに追記する用のルールの例を記しておきます。
## DuckDB-first Strategy
* **Always try DuckDB first**
* Handles joins, window functions, type inference, large files, and complex queries.
* Use `read_csv_auto('file.csv')` as default.
* For date/time strings, cast explicitly with `STRPTIME()`.
* **Fallback to awk only if**
* Single CSV, no joins/date ops, trivial filtering or first-order stats.
* Dataset size < 50k rows and < 50 columns.
* Use robust parsing:
```bash
awk -F, -v OFS=, -v FPAT='([^,]+)|(\"([^\"]|\"\")*\")' '...'
```
* **Failure policy**
* If the same approach fails twice, stop retrying.
* Summarize error + hypothesis, then switch strategy.
* **Operational tips**
* Remove BOM: `sed -i '1s/^\xEF\xBB\xBF//' *.csv`
Discussion