門脇
前回はPolarsの基本的な機能について紹介しました。今回は前回からの変更点や、その他の機能についても紹介します。
Polarsとは
前回の繰り返しになりますが、PolarsはPythonでデータ分析に使用されるpandasと同様の
バージョン1.
- Github:https://
github. com/ pola-rs/ polars - ユーザーガイド:https://
docs. pola. rs/ - Python APIリファレンス:https://
docs. pola. rs/ api/ python/ stable/ reference/
前回の記事からの変更点
まず最初に前回記事からの変更点について簡単に解説します。
前回記事ではPythonおよびPolarsのバージョンは以下を使用していました。
- Python 3.
11. 1 - Polars 0.
16. 1
今回の執筆時点で使用しているバージョンは以下のとおりです。
- Python 3.
12. 4 - Polars 1.
5.0
0.
また、未確認ですがアップグレードツールも存在しているようですので、興味のある方は試してみてください。
以降は、前回記事からの変更点について具体的に説明していきます。
データフレームの結合、マージにおけるキーにnull値を使用する
前回記事の内容では、null値をキーにした結合について触れておりませんが、バージョン0.
実際にサンプルスクリプトを使用して見ていきましょう。以下のようにnull値を含むデータフレームを結合してみます。
上記のスクリプトをバージョン0.
バージョン0.
Polars 1.join_
を引数として指定することで、同じ結果を得ることができます。
print(df1.join(df2, on="writer", how="inner", join_nulls=True))
また、バージョン0.how=outer
とした場合の結果が異なります。
print(df1.join(df2, on="writer", how="outer"))
バージョン0.
執筆時点のバージョン1.value=5
とvalue_
が同一行として結合されていません。このことからもわかるように、null値はそれぞれ別の値として扱われる点について注意が必要です。
なお、バージョン1.how="full", coalesce=True, join_
とすることでバージョン0.
print(df1.join(df2, on="writer", how="full", coalesce=True, join_nulls=True))
列に対する処理
続いては列に対する処理の変更について説明します。前回の記事ではpandasと同様に.apply()
メソッドを使用すると説明していますが、バージョン0..map_
メソッドに変更されました。
サンプルスクリプトは前回記事とほとんど同じですが、.apply()
の代わりに.map_
を使用しています。その他、引数return_
ではPolarsDataTypeで戻り値の型を指定することが推奨されるようになったため、文字列型であるpl.
を指定しています。
.map_
のように命名規則の戦略変更に伴う変更は、他にも以下などがあります
変更前 | 変更後 | 用途 |
---|---|---|
Series/ |
rolling_ |
シリーズに対してウィンドウサイズを指定した処理を適用する |
DataFrame. |
map_ |
データフレームの各行に対して処理を適用する |
GroupBy. |
map_ |
group_ |
その他のよく使う機能
これまでは前回記事に関連する変更点を中心に解説してきました。ここからは、前回記事では紹介しきれなかったPolarsの使い方について紹介します。
polars.when()で条件に応じてデータを操作する
Polarsではif-else文の条件式を.when()
、.then()
、.otherwise()
というメソッドにより実現できます。それぞれのメソッドに記述する内容をまとめると以下になります。
メソッド | 内容 |
---|---|
.when() |
条件式を定義する |
.then() |
.when() の条件式がTrue |
.otherwise() |
.when() の条件式がFalse |
これらのメソッドを使用することで、データフレームの列に対して.when()
で定義した条件に従ってデータの操作を行えます。たとえば以下のサンプルスクリプトでは、楽器名と演奏者数のデータフレームに対して条件により
.then()
や.otherwise()
で使用されているpl.
はリテラルを返すメソッドです。サンプルスクリプトでは演奏者数が1の場合に"Solo"が返され、それ以外の場合は"Ensemble"が返されます。実際の実行結果は以下になります。
$ python example03.py shape: (5, 3) ┌─────────────┬─────────┬──────────┐ │ Instruments ┆ Players ┆ Group │ │ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ str │ ╞═════════════╪═════════╪══════════╡ │ Violin ┆ 5 ┆ Ensemble │ │ Trombone ┆ 1 ┆ Solo │ │ Flute ┆ 3 ┆ Ensemble │ │ Cello ┆ 2 ┆ Ensemble │ │ Trumpet ┆ 1 ┆ Solo │ └─────────────┴─────────┴──────────┘
また、.when()
に複数の条件を設定したい場合は、以下の論理演算子を使用して条件を連結します。
&
:AND(かつ) |
:OR(または)
上記の演算子はpandasでも同様に使用できますが、Polarsでは&
演算子を暗黙としており省略することができます。
たとえば、以下の2行の実行結果は同じです。
pl.when((pl.col("Players") == 1) & (pl.col("Instruments") == "Trumpet"))
pl.when(pl.col("Players") == 1, pl.col("Instruments") == "Trumpet")
また、以下のように.when()
- .then()
を連鎖することで、if-elif-elseのような条件を設定することもできます。
df = df.with_columns(
[
pl.when(pl.col("Players") == 1) # 条件: 演奏者数が1の場合
.then(pl.lit("Solo")) # 'Solo' を追加
.when(pl.col("Players") == 2) # 条件: 演奏者数が2の場合
.then(pl.lit("Duo")) # 'Duo' を追加
.otherwise(pl.lit("Ensemble")) # それ以外の場合は 'Ensemble' を追加
.alias("Group") # 列名を 'Group' に設定
]
)
実行した結果は以下になります。
┌─────────────┬─────────┬──────────┐ │ Instruments ┆ Players ┆ Group │ │ --- ┆ --- ┆ --- │ │ str ┆ i64 ┆ str │ ╞═════════════╪═════════╪══════════╡ │ Violin ┆ 5 ┆ Ensemble │ │ Trombone ┆ 1 ┆ Solo │ │ Flute ┆ 3 ┆ Ensemble │ │ Cello ┆ 2 ┆ Duo │ │ Trumpet ┆ 1 ┆ Solo │ └─────────────┴─────────┴──────────┘
.when().then().otherwise()
はデータフレーム内で柔軟な条件付きロジックを実装する際に便利な機能です。ぜひ試してみてください。
DataFrame.sql()、polars.sql()でデータフレームに対するSQLクエリを実行
ここからは、SQLクエリを直接データフレームに対して実行できる.sql()
メソッドについて紹介します。
バージョン1.DataFrame.
メソッドが提供されました。また、新たに追加された polars.
メソッドでは、グローバルネームスペースの変数を扱えるようになり、複数のデータフレームに対してクエリを実行できるようになりました。
2つのメソッドについて、具体的に見ていきます。
DataFrame.sql()
以下のサンプルスクリプトでは、データフレーム内の特定の楽器であるTrombone、Trumpetを条件として抽出し、演奏者数を2倍にする処理をSQLクエリで実行しています。
一般的なSQL文と異なる点として、FROM句に self
というキーワードが指定されています。selfはクエリの対象となるデータフレーム自身を指すテーブル名のデフォルト値です。DataFrame.table_
で指定することもできます。
サンプルスクリプトの実行結果は以下のとおりです。
$ python example04.py shape: (2, 2) ┌─────────────┬─────────┐ │ Instruments ┆ Players │ │ --- ┆ --- │ │ str ┆ i64 │ ╞═════════════╪═════════╡ │ Trombone ┆ 2 │ │ Trumpet ┆ 2 │ └─────────────┴─────────┘
サンプルコードだけを見ると同じような条件でデータフレームを操作することは、 .filter()
や .when()
などのメソッドを使用して実現可能と思うかもしれません。この指摘についてはその通りなのですが、内容によってはメソッドチェーンよりもSQLの方がコードの可読性が高く、Polarsに慣れていない人でもわかりやすいケースもありそうです。
サンプルスクリプトを使用して実際に見比べてみましょう。スクリプトでは以下の処理を行っています。
- 取得対象とする列は
「Instruments」 と 「Players」 のみ (「Category」 を除外) - 「Instruments」
列の値が ['Trumpet', 'Trombone']
の場合、「Players」 列の数値を2倍にする - 「Players」
列がnullの場合は 「0」 を返す
結果はどちらも以下のようになりました。
┌─────────────┬─────────┐ │ Instruments ┆ Players │ │ --- ┆ --- │ │ str ┆ i64 │ ╞═════════════╪═════════╡ │ Violin ┆ 5 │ │ Trombone ┆ 0 │ │ Flute ┆ 3 │ │ Cello ┆ 0 │ │ Trumpet ┆ 2 │ └─────────────┴─────────┘
メソッドチェーンとSQLクエリのサンプルスクリプトはどちらも同じ結果を返しますが、SQLクエリのほうが直感的でわかりやすいのではないでしょうか。
データフレームオブジェクトに対するSQLクエリの実行は.filter()
メソッドと組み合わせて使用することもでき、とても便利ですのでぜひ試してみてください。なお、Polarsで利用可能なSQLインタフェースについては、以下のドキュメントを参照してください。
polars.sql()
polars.
もデータフレームに対するSQLクエリを実行できるメソッドですが、 この関数はPolarsネームスペースの変数にアクセスできるようになっており、複数のデータフレームを操作する場合に便利です。たとえばクロスジョインやサブクエリなど、Polarsのメソッドでも少し複雑な処理を実現したいケースなどでの利用が考えられます。
以下のサンプルでは、フルーツごとの最高価格を得るために2つのデータフレームを使用する処理をSQLクエリを使用して実現しています。DataFrame.
ではFROM句にselfキーワードが使用されていましたが、polars.
ではデータフレームオブジェクトの変数名をそのまま使用します。
SQLクエリの実行においては、.collect()
メソッドが使用されています。これは、戻り値のデータフレームオブジェクトがLazyFrameという遅延評価データフレーム[2]であることを意味しています。遅延評価では.collect()
メソッドが使用されますが、実行結果をすぐに返す先行評価eager=True
を指定することもできます。
df = pl.sql("""
SELECT
..〈省略〉
JOIN fruits_df f ON sub.fruit_id = f.fruit_id
""", eager=True)
上記のサンプルスクリプトも、メソッドチェーンを使用しても実現可能です。どちらが利用しやすいかは人によって異なると思いますが、利用シーンに合わせて検討していただければと思います。
# メソッドチェーンで同じ結果を得る方法
max_costs = costs_df.group_by("fruit_id").agg(pl.col("cost").max().alias("max_cost"))
# max_costsを果物データフレームと結合
df = max_costs.join(fruits_df, on="fruit_id").select(["fruit_name", "max_cost"])
Polars 1.0のリリースで注目される今後
Polarsの開発は、1.
1.
新しいエンジン設計
最も注目すべき点として、ストリーミングエンジンの再設計が行われているようです。具体的な実装についてはまだ明らかにはされていませんが、将来はモーセル駆動型の並列性[3]とRustの非同期ステートマシンを組み合わせた設計になるとのことです。これにより、NUMA
執筆時点ではまだ開発段階のStreaming APIですが、気になったのでkaggleのデータセットを使用して繰り返し連結した70GB程度のCSVファイルで試してみたところ、Streaming APIでは物理メモリサイズを超えるデータを問題なく処理することができました。検証に使ったサンプルスクリプトは以下になります。
import polars as pl
# チャンクサイズを指定
pl.Config.set_streaming_chunk_size(30000)
# CSVファイルの読み込みとフィルタの実行
q1 = (
pl.scan_csv("Books_rating_large.csv")
.select(pl.col("review/score"))
.filter(pl.col("review/score") < 2)
)
df = q1.collect(streaming=True) # Streaming modeで実行
print(df.select(pl.col("review/score")).head(5))
print(df.select(pl.col("review/score").len()))
検証は16GBの物理メモリを持つLinux環境で行いました。大規模なデータセットを使用メモリを抑えながら効率的に処理できるのは、とても魅力的ですね。
NVIDIA RAPIDSによるGPUアクセラレーション
PolarsにGPUアクセラレーションが導入されます。すでにメソッドのいくつかでdevice="gpu"
として実行できるようになっているものもあり、さらなる進化が期待されます。
Polarsクラウド
Polarsをマネージドサービスとして利用できるクラウドサービスの提供が予定されており、今年中にベータテストが開始される見込みのようです。また、オープンソースユーザーにもメリットがあるように改良されるとのことで、どのようなリリースになるのか楽しみですね。
1.0リリース後に追加されたその他の機能
先述の内容以外にも1.
- Polarsバージョン1.
0アップグレードガイドの内容 (筆者が気になったもの) - NumPy 2.
0をサポート - Hugging Faceからのデータセットの直接読み込みサポート
まとめ
今回の記事では、Polars 1.
データ処理の世界において、スピードと効率性は非常に重要です。メモリ消費を抑えつつ、利便性に優れ、高いパフォーマンスを得ることができるPolarsをぜひ試してみてください。