Polarsが2.0のリリース候補版を出しました。データ分析ライブラリのメジャーバージョン更新というと、新機能の盛り込みを想像しがちですが、今回の主眼はむしろ逆で、既定値の見直しとAPIの整理です。特にLazyFrameの処理が標準で streaming engine に乗るようになる点は、使い方によってはかなり効きます。表向きは“boring”な更新でも、実際には日々のメモリ使用量や速度、そして失敗の見つけやすさを変える話です。
Polars 2.0の一番大きな変更は、LazyFrame で collect() したときの既定エンジンが streaming engine になることです。これまでのように全部をメモリに載せる前提ではなく、流しながら処理するので、多くのクエリでメモリ使用量と性能が大きく改善する、と説明されています。投稿では、集計すると全体として streaming engine は簡単に 5倍速くなると見込んでいます。
ただし、ここには代償もあります。join、group_by、unpivot などの操作では、結果の行順が以前のように保証されません。行の順番まで観測したい場合は maintain_order=True を明示して指定できますし、これまでどおり in-memory エンジンを使いたい人は、全体設定で engine_affinity を in-memory に変える方法もあります。個別のクエリだけ in-memory にすることも可能です。
Polars が今回 major version を切った理由も、そのあたりにあります。古い設計判断が足かせになっている部分を外し、より多くの人にとって自然な既定値へ寄せる。そのためのバージョン更新だというわけです。あわせて、移行ガイドも公開されており、この記事ではその中でも目立つ点だけを紹介しています。
2.0 では strictness、つまり「曖昧な処理を黙ってやらない」方向が強まっています。Polars は以前から fail fast を重視してきましたが、今回の説明では、AI による開発が広がったことでこの性格がさらに重要になったとも述べています。collect_schema() を使えば、実データを materialize せずに型やスキーマの不一致を確認でき、クエリの形を早く検証できます。
例として挙げられているのが is_in の型変換です。以前は異なる型同士でも共通の型に暗黙変換していましたが、それが損失のある変換でも通ってしまうことがあった。記事では、Float64 に落ちた大きなIDと Int64 のユーザーIDを比較したとき、丸め込みで誤検知が起きうる例を示しています。2.0 ではこうしたケースは InvalidOperationError になり、必要なら明示的に cast せよ、という扱いです。
横方向の concat も変わります。行数が違う DataFrame を横に結合したとき、以前は足りない部分を null で埋めていましたが、2.0 ではまず ShapeError を出します。もし本当に null 埋めが欲しいなら、how="horizontal_extend" を明示する必要があります。意図をコード上で見えるようにする、という考え方です。
さらに、曖昧だった cast の一部は専用メソッドに置き換えられました。Enum や categorical と整数の相互変換は .cat.to(dtype) や .cat.physical() に分かれ、文字列から Date や Datetime への変換は .str.to_date() や .str.to_datetime() を使います。単に型を変えるのではなく、どう解釈して読むかを明示しろ、という設計です。
Polars 2.0 では、古い属性や引数を消したときのエラーも分かりやすくなっています。新しく追加されたのは AttributeRemovedError と ArgumentRemovedError で、消えたメソッドや引数を使ったときに、何が削除され、代わりに何を使うべきかを案内します。
記事の例では、melt() を呼ぶと LazyFrame.unpivot を使うようにと返され、id_vars や value_vars の代わりに index と on を使えと示されます。別の例では、DataFrame.join に join_nulls=True を渡すと、それは 2.0 で nulls_equal に改名されたと教えてくれます。古いコードをただ切り捨てるのではなく、移行先を機械的に示す作りです。
ここで面白いのは、Polars が「破壊的変更をしたい」わけではない点です。むしろ長く非推奨だったものはかなり前から告知していたはずで、追随していた利用者なら大きく困らないだろう、と記事は言っています。つまり、今回の更新は大改造というより、積み残した曖昧さの整理に近い。大きく壊すのではなく、壊れやすい部分を先に明文化しておく、という姿勢が見えます。
私は、このリリースの語り口がかなり正直だと思いました。Polars 側は繰り返し「大きな新機能を並べる版ではない」「boring であってほしい」と言っていますが、実際には相当実務寄りの変更です。データ処理ライブラリで本当に困るのは、派手な新機能よりも、メモリが足りない、順序が崩れる、型が勝手に変わる、そして後から原因が分からない、という事故だからです。2.0 はそこをまとめて潰しにきている。
特に streaming engine の既定化は、データ量が増えるほど効いてきます。小さな分析では差が見えにくくても、途中でメモリが跳ねる処理や、バッチが詰まる処理ではかなり意味があるはずです。一方で、順序保証が外れるのは初心者ほど戸惑いやすい。だからこそ maintain_order や in-memory の逃げ道を残しているのは妥当です。速度を取るか、観測可能な順序を取るかを、利用者に選ばせる形になっています。
記事の中で、strictness の価値を AI-driven development と結びつけているのも印象的でした。これは単なる流行りの単語合わせではなく、かなり実感のある話だと思います。生成系の支援でクエリ自体は早く書けても、暗黙の型変換や null 埋めで結果が静かにずれると、間違いに気づくのが遅れる。Polars はそこで、実行の柔軟さより、失敗の早さを選んでいる。
この方針は、人間の保守にも効きます。コードレビューで「たぶん大丈夫」な箇所が減るからです。is_in で大きな整数が float に丸められて誤判定する例は、まさにそういう事故の説明でした。見た目は動いているのに、意味が壊れる。データ処理では最悪のパターンです。明示的な cast や専用メソッドを要求するのは面倒でも、結果的には読み手の負担を減らします。
最後に示されている今後の話も、かなり野心的です。streaming engine の proper out-of-core support、新しい IO-plugin design、より速いと見込む S3 reader、SQL coverage の改善、cost-based planner、join reordering、mmap の削除など、地味に見えて全部が基盤寄りです。しかも「新機能は ready になったらすぐ出すので、major version に閉じ込めない」と言っている。ここから先の 2.x は、見出しになりやすい機能より、内部の通り道を整える期間になるのではないかと思います。
この路線は、ライブラリの成熟としてはかなり健全です。大きな破壊を避けつつ、既定値を現代の使い方に合わせ、エラーを早く、説明を親切にし、性能の取りこぼしを減らす。派手ではないけれど、現場ではこういう更新のほうが効く。Polars 2.0 は、そのことをかなりはっきり示したリリース候補だと見ています。