Claude CodeでPolarsのデータ処理を書かせる勘所
Claude CodeにPolarsのデータ処理を書かせるときの勘所をまとめます。expressionとcontextの使い分け、lazy APIでの大規模データ対応、CLAUDE.mdでの規約指定を扱います。
Claude CodeにPolarsのコードを書かせると、pandasの感覚をそのまま持ち込んだコードが返ってくることがあります。構文としては動いても、Polars本来の速さを引き出せていないケースです。本稿では、expressionとcontextの使い分け、大きいデータでのlazy API、CLAUDE.mdでの規約指定という3つの軸で、Claude CodeにPolarsのデータ処理を書かせるときの勘所を扱います。コード例はPolars 1.44.2で実行した結果です。
Claude CodeにPolarsを使わせる前に何を伝えておくか
Polarsは、Rust実装のクエリエンジンを持つDataFrameライブラリです。PythonとRustでは、lazy APIを使えるようにする手順が違います。
Polarsの導入はPythonとRustで違う
Python
pip install polarsだけで、lazy APIをscan_csvなどからそのまま使えます。
Rust
cargo add polars -F lazyのようにfeaturesへlazyを足さないと、lazy APIは使えません。
ClaudeにRustコードを書かせるときは、この差を依頼文に添えます。たとえば「Rustで書く。Cargo.tomlのfeaturesにlazyを足してからlazy APIを使う」と一文入れておきます。
CLAUDE.mdは、セッションのたびに説明し直したくない事実を書いておく場所です。公式ドキュメントは、Claudeが同じ間違いを2度したときや、コードレビューでClaudeが知っておくべきだった点を指摘されたときを、CLAUDE.mdへの追記タイミングとして挙げています。Polarsを使うプロジェクトなら、pandas由来の書き方を避ける指示をあらかじめ書いておけます。
モノレポの一部だけがPolarsを使っている場合、規約をリポジトリ直下のCLAUDE.mdへ書くと他のディレクトリの作業にも常に読み込まれてしまいます。特定のパス配下にだけ規約を効かせたいなら、.claude/rules/に置いたルールファイルの先頭でpathsを指定できます。パスに合うファイルをClaudeが読み書きするときに、そのルールが読み込まれます。
---
paths:
- "pipelines/**/*.py"
---
# Polarsの規約
- DataFrame処理はPolarsのlazy API(`pl.scan_*` → `.collect()`)を既定にする
- `df.groupby(...)`のような旧来の書き方はしない。`df.group_by(...)`で書く
- pandas風の`df[df["col"] > 0]`ではなく`.filter(pl.col("col") > 0)`で書く
- 10万行を超えるファイルは`.collect(engine="streaming")`を使う(閾値は例です。環境のメモリで決めます)生成したスクリプトをBashで実行させる場面では、SQLを書かせるときと同じ権限設計の注意が要ります。許可ルールとフックの使い分けはClaude Code settings.json完全ガイドにまとめてあります。
pandasやgroupbyに戻らせないためにPolars公式が案内する手当
Polarsのユーザーガイドには、LLMがpandasのコードや無効なPolarsコードを返すことがあると明記した「Generating Polars code with LLMs」の章があります。公式が挙げる手当は5つです。
Polars公式が案内するLLM向けの手当
- 1
公式のPolars skillを入れる
エージェントにPolarsの知識を読み込ませ、より慣用的なPolarsコードを書かせるskillです。pandasコードの変換にも使えると案内されています。
- 2
Polarsの公式MCPサーバーをつなぐ
ユーザーガイドとAPIリファレンスをLLMが直接引けるリモートMCPサーバーです。URLは
https://mcp.pola.rs/mcpで、Claude Codeではclaude mcp add --transport http <名前> <URL>の形で追加できます(v2.1.289のclaude mcp add --helpの書式)。 - 3
既定のライブラリをプロンプトに書く
「Polarsを既定のDataFrameライブラリにする」と書いておくと、pandasでなくPolarsのコードが返りやすくなると説明されています。Claude Codeでは、前節のCLAUDE.mdがこの置き場所にあたります。
- 4
Web検索を有効にする
Web検索を使えるLLMなら、最新のPolars APIドキュメントを参照させられます。ただしWeb検索だけでは有効なコードは保証されません。モデルが事前学習の知識に自信を持っていると、検索結果を出力に反映しないことがあります。
- 5
正しい書き方の例を添える
公式の例では、平均を求める依頼が古い
groupby構文になりがちです。df.group_by("a").agg(pl.col("b").mean())のような例文を、できればWeb検索を有効にして添えると、有効なコードが返る頻度が上がるとされています。Web検索と例文は、組み合わせたほうが単独より効果的です。
groupbyが通らないのは実機でも確かめられます。Polars 1.44.2では次のエラーになります。
AttributeError: 'DataFrame' object has no attribute 'groupby'古い構文はエラーで気づけますが、問題は構文エラーにならないケースです。続く節からは、動いてしまうのに意図とずれるコードの見分け方を扱います。
expressionとcontextの使い分けをClaude Codeに指示する
PolarsのAPIは、expressionとcontextという2つの概念で組み立てます。expressionは列に対する変換の手順そのもので、contextはそれを実際に計算させる場所です。
例として、ECサイトの注文データを使います。
import polars as pl
import datetime as dt
orders = pl.DataFrame(
{
"order_id": [1001, 1002, 1003, 1004, 1005],
"customer": ["田中", "佐藤", "田中", "鈴木", "佐藤"],
"category": ["食品", "雑貨", "食品", "食品", "雑貨"],
"amount": [2400, 5800, 1200, 3600, 4200],
"ordered_at": [
dt.date(2026, 1, 15),
dt.date(2026, 2, 3),
dt.date(2026, 2, 20),
dt.date(2026, 3, 5),
dt.date(2026, 3, 18),
],
}
)selectは列を選び直したり計算したりするcontextです。
result = orders.select(
pl.col("customer"),
pl.col("ordered_at").dt.month().alias("month"),
(pl.col("amount") * 1.1).round(0).alias("amount_with_tax"),
)with_columnsはselectと似ていますが、元の列を残したまま新しい列を追加します。既存の列に税込金額や月の列を足したいだけなら、こちらを使うようClaudeに指示したほうが結果が扱いやすくなります。
orders_enriched = orders.with_columns(
month=pl.col("ordered_at").dt.month(),
amount_with_tax=(pl.col("amount") * 1.1).round(0),
)filterは条件に合う行だけを残すcontextです。
result = orders.filter(
(pl.col("category") == "食品") & (pl.col("amount") > 2000)
)group_byはグループごとの集計に使い、aggと組み合わせます。
result = orders.group_by("customer", maintain_order=True).agg(
pl.len().alias("注文件数"),
pl.col("amount").sum().alias("合計金額"),
)集計した結果をファイルへ書き出す場面もよくあります。Polarsはcsv・json・parquetといった主要な形式の読み書きに対応しており、write_csvで書き出した内容はread_csvで読み返せます。
result.write_csv("summary.csv")
summary = pl.read_csv("summary.csv", try_parse_dates=True)繰り返し読み込むデータなら、列指向のparquet形式(write_parquet / read_parquet)という選択肢もあります。出力形式は依頼文かCLAUDE.mdに書いておくと、後工程が読む形式を揃えられます。
expression単体は計算の手順にすぎず、contextに置いて初めて結果が計算されます。pl.col("amount") * 1.1という式をそのままprintすると、値の代わりに式の中身が出ます。
[(col("amount")) * (dyn float: 1.1)]生成コードの出力にこの形が混じっていたら、contextに渡し忘れたexpressionです。
大きいデータではlazy APIを使うようClaude Codeに指示する
Polarsのlazy APIは、コードを1行ずつ即座に実行せず、クエリ全体を最適化してから実行する仕組みです。公式ドキュメントは、lazy APIを使う理由として、クエリオプティマイザによる自動最適化、メモリに載らないデータをストリーミングで扱える点、データ処理前にスキーマの誤りを検出できる点の3つを挙げています。
pl.read_csvで読み込んで加工するeagerな書き方は、小さいファイルの確認にはそのままで十分です。ファイルが大きくなる、あるいは複数の変換を重ねるなら、pl.scan_csvで始めるlazyな書き方に切り替えるようClaudeに伝えます。
result = (
pl.scan_csv("orders.csv")
.filter(pl.col("category") == "食品")
.group_by("customer")
.agg(pl.col("amount").sum().alias("合計金額"))
.collect(engine="streaming")
)collect(engine="streaming")を指定すると、Polarsはデータをバッチに分けて処理し、メモリに一度に載らない量のファイルも扱えます。既定のcollect()はデータ全体を1バッチとして処理するため、ピーク時のメモリ使用量が手元の環境に収まるかを先に確認する必要があります。
クエリを開発している最中は、全件処理を待たずに構文の間違いへ早く気づきたい場面が多くなります。公式は、.head(n)をクエリの先頭に置き、末尾で.collect()する書き方を案内しています。
q = (
pl.scan_csv("orders.csv")
.head(100)
.filter(pl.col("category") == "食品")
.collect()
)ただし、公式は部分データでの集計や絞り込みの結果が、全体の結果を代表しないことがあると注意しています。200,000行のCSVで実際に比べると、差は桁違いでした。
田中さんの食品の合計金額(200,000行のCSV、Polars 1.44.2)
head(100)を挟んだ結果
60,446
先頭100行だけの集計
全件の結果
117,540,365
collect(engine="streaming")
.head(n)は構文とスキーマの確認に使い、数値は全件で出し直す運用になります。Claudeに「検証用の数字を報告に使わない」と伝えておく価値があります。
同じLazyFrameから複数の集計結果がほしいときは、pl.collect_allにまとめて渡す方法があります。LazyFrameは計算の計画にすぎず、共通する前段の計算を自動でキャッシュしないため、個別に.collect()するとそのたびにやり直されます。
lf = pl.scan_csv("orders.csv")
by_customer = lf.group_by("customer").agg(pl.col("amount").sum())
by_category = lf.group_by("category").agg(pl.col("amount").sum())
pl.collect_all([by_customer, by_category])Polarsはcsvやparquetのようなファイル形式だけでなく、postgresやmysqlといったデータベースからも直接読み込めます。Claude CodeからMCP経由でデータベースに接続し、読み取り専用の権限で処理を任せたい場合の設定はデータベースMCPサーバーの読み取り専用設定を5製品で比較するで扱っています。
複数のデータフレームを組み合わせる
Polarsは複数のデータフレームを結合する仕組みも備えています。ここでは、結合(join)と連結(concat)という性格の違う2つの操作を扱います。
注文データに顧客マスタを結合する例です。
customers = pl.DataFrame(
{
"customer": ["田中", "佐藤", "鈴木"],
"prefecture": ["東京都", "大阪府", "愛知県"],
"member_since": [2021, 2019, 2023],
}
)
joined = orders.join(customers, on="customer", how="left")結合キーの値が相手側で重複していると、結合後の行数がその分だけ増えます。
行の増殖はエラーにならないので、Claudeにvalidate引数を使わせると途中で止められます。たとえば顧客マスタに「田中」が2行ある状態でorders.join(customers, on="customer", how="left", validate="m:1")を実行すると、Polars 1.44.2ではComputeError: join keys did not fulfill m:1 validationで止まります。validateなしで同じ結合をすると、注文5行が7行に増えました。
月ごとに分かれたCSVを1つにまとめるような場面では、結合ではなく連結を使います。
orders_march = pl.DataFrame(
{
"order_id": [1006, 1007],
"customer": ["鈴木", "田中"],
"category": ["雑貨", "食品"],
"amount": [3100, 2800],
"ordered_at": [dt.date(2026, 3, 25), dt.date(2026, 3, 29)],
}
)
pl.concat([orders, orders_march], how="vertical")Polarsは縦方向・横方向・対角方向の3種類の連結を用意しています。列構成が同じ複数ファイルを積み上げるなら縦方向、同じ行数のデータフレームを横に並べて列を増やすなら横方向です。列名が違うデータを縦方向で連結すると、ShapeError: unable to vstack, column names don't matchで止まります。列が足りない行をnullで埋めたいときは、how="diagonal"を指定します。
Claude Codeが書いたPolarsコードで踏みやすいつまずき
Claude Codeが書いたPolarsコードには、構文エラーにならず実行できてしまうつまずきがあります。
| つまずき | 起きること | 避け方 |
|---|---|---|
| expressionをcontextの外で評価しようとする | 起きることprintしても計算済みの値が出ない | 避け方selectやfilterなどのcontextに必ず入れる |
| LazyFrameを複数箇所で使い回す | 起きること参照するたびに前段の計算がやり直される | 避け方pl.collect_allでまとめて評価する |
group_byの順序をあてにする | 起きること実行のたびに行の並びが変わる | 避け方maintain_order=Trueを明示する |
| RustでPython向けの説明をそのまま使う | 起きることlazy API関連が使えない | 避け方Cargo.tomlのfeaturesにlazyを足す |
group_byの結果順序は既定では保証されません。同じ4顧客の集計を3回実行すると、maintain_orderなしでは['高橋', '田中', '佐藤', '鈴木']、['高橋', '鈴木', '佐藤', '田中']、['田中', '鈴木', '佐藤', '高橋']と並びが変わり、maintain_order=Trueでは3回とも['佐藤', '田中', '鈴木', '高橋']でした。クエリプランの表示もAGGREGATE[maintain_order: false]となっています。並び順が表示や後続処理に影響する集計では、maintain_order=Trueを指示に含めます。合計値だけを使うなら不要です。
生成コードの計画を実行前に読む
lazy APIなら、データを読む前にexplain()でクエリプランを出せます。200,000行のCSVに対する食品の集計では、フィルターがSELECTIONとしてCSVの読み込み側に押し込まれ、4列のうち3列だけを読むプランになりました。
AGGREGATE[maintain_order: false]
[col("amount").sum()] BY [col("customer")]
FROM
simple π 2/2 ["customer", "amount"]
Csv SCAN [orders.csv]
PROJECT 3/4 COLUMNS
SELECTION: col("category") == "食品"scan_csvではなくread_csvで全件を読み込んでいるコードは、このプランを出せません。存在しない列を指定した場合も、lazyならcollect_schema()の段階でColumnNotFoundError: unable to find column "nope"; valid columns: [...]と列名の候補つきで止まります。
使い分け早見表
どの書き方を選ぶべきかは、扱うデータの大きさと開発の段階によって変わります。
| 状況 | Claudeに書かせる形 | 理由 |
|---|---|---|
| 小さいCSVをすぐ確認したい | Claudeに書かせる形pl.read_csv(eager) | 理由クエリ最適化の恩恵が小さく、即座に結果を見たい場面に向く |
| メモリに載らない大きいファイル | Claudeに書かせる形pl.scan_csv + .collect(engine="streaming") | 理由データをバッチ処理し、メモリ使用量を抑えられる |
| クエリを開発・デバッグしている最中 | Claudeに書かせる形.head(n)を挟んで.collect() | 理由全件処理を待たずに構文ミスへ気づける |
| 同じLazyFrameから複数の集計がほしい | Claudeに書かせる形pl.collect_all([...]) | 理由共通の前段計算を1回にまとめ、二重計算を避けられる |
| 順序が結果に影響する集計 | Claudeに書かせる形group_by(..., maintain_order=True) | 理由既定では保証されない行の並びを固定する |
生成したPolarsコードを保存後にどう確認するか
生成されたコードをその場で信用せず、保存直後に自動でテストやlintを走らせる仕組みを作っておくと、Claudeが書いたPolarsコードにも同じ安心感が持てます。ファイル保存のたびに指定のコマンドを走らせるPostToolUseフックの設定手順はPostToolUse hookでツール実行後の後処理を自動化するにまとめています。
実行経路によって確認すべきポイントが変わるという考え方は、SQLや正規表現をClaudeに書かせるときと同じです。特定領域のコードをClaude Codeに書かせるときの勘所はClaude Code SQLと正規表現を安全に書かせる勘所でも扱っています。
まとめ
Claude CodeにPolarsを書かせるときは、pandas風に動いてしまうコードを「エラーにならない順」に潰す発想が要ります。順序の固定、結合のvalidate、部分データの数字を報告に使わない、の3点を規約に書いておけば、レビューで見るべき箇所が絞れます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。