Pandas からの移行
ここでは、Pandas の経験がある人が Polars を試す際に知っておくべき 重要なポイントを説明します。Polars と Pandas それぞれのライブラリが 基礎としている概念の違いと、Pandas と比較した Polars のコードの書き方の違いを 説明します。
Polars と Pandas の概念の違い
Polars にはマルチインデックス/インデックスがない
Pandas は各行にインデックスでラベルが付与されます。Polars にはインデックスがなく、 各行はテーブルの中での整数位置によってインデックスされます。
Polars は予測可能な結果と読みやすいクエリを目指しており、インデックスはそれらの目的に役立たないと考えています。
クエリの意味は、インデックスの状態や reset_index
の呼び出しによって変わるべきではないと信じています。
Polarsのデータフレームは常に2次元の異種データ型のテーブルです。データ型にはネストが存在する可能性がありますが、 テーブル自体にはネストはありません。 リサンプリングなどの操作は、明示的にどの列に対して行うかを示す専用の関数やメソッド(「動詞」のようなもの)で行います。 したがって、インデックスがないことで、より単純で明示的で読みやすく、 エラーが少なくなると確信しています。
ただし、データベースで知られている「インデックス」のデータ構造は、Polars の最適化技術として使用されます。
Polars はメモリ上で Apache Arrow の配列を使用する一方、Pandas は NumPy 配列を使用する
Polars はメモリ上で Apache Arrow の配列を使用する一方で、 Pandas は NumPy 配列を使用します。Apache Arrow は、データ読み込み時間の短縮、 メモリ使用量の削減、計算の高速化などを実現する 新興の列指向メモリ分析標準です。
Polarsは to_numpy
メソッドでデータを NumPy 形式に変換できます。
Polars は Pandas よりも並列処理をサポートする
Polarsは Rust の並行性の強力なサポートを活用して、多くの操作を並列に実行できます。
Pandas にも一部の操作で並列処理があるものの、
ライブラリの中核部分は単一スレッドであり、並列処理のためにはDask
などの
ライブラリを追加で使う必要があります。
Polars は遅延評価クエリとクエリ最適化が可能
即時評価は、コードを実行するとすぐにコードが評価されます。 遅延評価は、行のコードを実行することで、基礎となるロジックがクエリ計画に追加され、 評価されないことを意味します。
Polars は即時評価と遅延評価をサポートしていますが、 pandas は即時評価のみを サポートしています。遅延評価モードは強力で、Polars はクエリ計画を調べ、 クエリを高速化したり、メモリ使用量を削減する方法を見つけると、 自動クエリ最適化を行います。
Dask
も、クエリ計画を生成する際に遅延評価をサポートしています。ただし、Dask
は
クエリ計画に対してクエリ最適化を行いません。
主要な構文の違い
Pandas から移行してきたユーザーは一般に1つのことを知る必要があります...
polars != pandas
もしもあなたの Polars のコードが Pandas のコードのように見える場合、実行はできるかもしれませんが、 おそらく適切な速度で実行されることはないでしょう。
いくつかの典型的な Pandas コードを見て、それを Polars でどのように書き換えるか見ていきましょう。
データの選択
Polars にはインデックスがないため、.loc
や iloc
メソッドが存在せず、
SettingWithCopyWarning
も Polars には存在しません。
しかし、Polars でデータを選択する最良の方法は、expression API を使用することです。 たとえば、Pandas で列を選択したい場合、次のいずれかを行うことができます:
df['a']
df.loc[:,'a']
しかし、Polars では .select
メソッドを使用します:
df.select('a')
値に基づいて行を選択したい場合は、
Polars で .filter
メソッドを使用します:
df.filter(pl.col('a') < 10)
下記の式に関するセクションで述べられているように、Polars は .select
および
filter
での操作を並列に実行することができ、データを選択する基準の全てのセットに対して
クエリ最適化を行うことができます。
遅延評価を利用する
遅延評価モードでの作業は単純であり、Polars では遅延モードが クエリ最適化を可能にするため、デフォルトとすべきです。
遅延モードでの実行は、暗黙的に遅延関数(scan_csv
など)を使用するか、
明示的に lazy
メソッドを使用することで行えます。
次のシンプルな例を考えます。ディスクから CSV ファイルを読み込んでグループ化します。
CSV ファイルには数多くの列がありますが、私たちは id1
の列でグループ化して、
値列(v1
)で合計を出したいだけです。pandas では次のようになります:
df = pd.read_csv(csv_file, usecols=['id1','v1'])
grouped_df = df.loc[:,['id1','v1']].groupby('id1').sum('v1')
Polars ではクエリを遅延モードで構築してクエリ最適化を行い、
即時的な Pandas 関数の read_csv
を
暗黙的に遅延する Polars 関数の scan_csv
に置き換えて評価できます:
df = pl.scan_csv(csv_file)
grouped_df = df.group_by('id1').agg(pl.col('v1').sum()).collect()
Polars はこのクエリを id1
および v1
列のみが関連していると特定し、
これらの列のみを CSV から読み込むよう最適化します。2行目の最後で .collect
メソッドを呼び出すことで、クエリをその時点で評価するよう Polars に指示します。
このクエリを即時モードで実行したい場合は、
Polars コードで scan_csv
を read_csv
に置き換えるだけです。
遅延評価の使用については、 lazy API の章で詳しく読むことができます。
エクスプレッションを使う
典型的な pandas スクリプトは、逐次的に実行される複数のデータ変換で構成されています。 しかし、Polars ではこれらの変換をエクスプレッションを使って 並列に実行することができます。
カラムの割り当て
df
という DataFrame に value
というカラムがあり、
value
を10倍した tenXValue
という新しいカラム、
および value
列を100倍した hundredXValue
という新しいカラムを追加したいとします。
pandas では次のようになります:
df.assign(
tenXValue=lambda df_: df_.value * 10,
hundredXValue=lambda df_: df_.value * 100
)
これらのカラムの割り当ては逐次的に実行されます。
Polars では with_columns
メソッドを使ってカラムを追加します:
df.with_columns(
tenXValue=pl.col("value") * 10,
hundredXValue=pl.col("value") * 100,
)
これらのカラムの割り当ては屁入れで実行されます。
条件に基づくカラムの割り当て
次のケースでは、カラム a
、b
、c
を持つ dataframe df
があったとします。
条件に基づいてカラム a
の値を割り当てしなおしたいと考えます。カラム c
の値が 2 に等しい場合、
カラム a
の値をカラム b
の値に置き換えます。
pandas では次のようになります:
df.assign(a=lambda df_: df_.a.where(df_.c != 2, df_.b))
一方で Polars では次のようになります:
df.with_columns(
pl.when(pl.col("c") == 2)
.then(pl.col("b"))
.otherwise(pl.col("a")).alias("a")
)
Polars は if -> then -> otherwise
の各ブランチを並列に計算することができます。
これは、ブランチの計算が高コストになる場合に価値があります。
フィルタリング
いくつかの条件に基づいて住宅データを持つ Dataframe df
をフィルタリングしたいとします。
pandas では query
メソッドにブール式を渡して Dataframe をフィルタリングします:
df.query("m2_living > 2500 and price < 300000")
またはマスクを直接評価します:
df[(df["m2_living"] > 2500) & (df["price"] < 300000)]
一方で Polars は filter
メソッドを呼びます:
df.filter(
(pl.col("m2_living") > 2500) & (pl.col("price") < 300000)
)
Polars のクエリ最適化エンジンは、複数のフィルターを別々に記述したことを検出し、 最適化された計画でそれらを1つのフィルターに組み合わせることができます。
pandas の変換
pandas のドキュメントでは、transform
と呼ばれるグループ化に対する操作が示されています。
この場合、DataFrame df
があり、各グループの行数を示す
新しい列が必要です。
pandas では次のようになります:
df = pd.DataFrame({
"c": [1, 1, 1, 2, 2, 2, 2],
"type": ["m", "n", "o", "m", "m", "n", "n"],
})
df["size"] = df.groupby("c")["type"].transform(len)
ここで pandas は "c"
でグループ化を行い、"type"
カラムを取り、グループの長さを計算し、
その結果を元の DataFrame
に戻して以下を生成します:
c type size
0 1 m 3
1 1 n 3
2 1 o 3
3 2 m 4
4 2 m 4
5 2 n 4
6 2 n 4
Polars では同じことを window
関数で実現できます。
df.with_columns(
pl.col("type").count().over("c").alias("size")
)
shape: (7, 3)
┌─────┬──────┬──────┐
│ c ┆ type ┆ size │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ u32 │
╞═════╪══════╪══════╡
│ 1 ┆ m ┆ 3 │
│ 1 ┆ n ┆ 3 │
│ 1 ┆ o ┆ 3 │
│ 2 ┆ m ┆ 4 │
│ 2 ┆ m ┆ 4 │
│ 2 ┆ n ┆ 4 │
│ 2 ┆ n ┆ 4 │
└─────┴──────┴──────┘
単一の式に全ての操作を格納できるため、複数の window
関数を組み合わせたり、
異なるグループを組み合わせたりすることができます!
同じグループに適用されるwindowエクスプレッションは Polars によってキャッシュされるため、
単一の with_columns
にそれらを格納することは便利であり、かつ 最適です。次の例では、
"c"
に対してグループ統計を2回計算するケースを見ていきます:
df.with_columns(
pl.col("c").count().over("c").alias("size"),
pl.col("c").sum().over("type").alias("sum"),
pl.col("type").reverse().over("c").alias("reverse_type")
)
shape: (7, 5)
┌─────┬──────┬──────┬─────┬──────────────┐
│ c ┆ type ┆ size ┆ sum ┆ reverse_type │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ u32 ┆ i64 ┆ str │
╞═════╪══════╪══════╪═════╪══════════════╡
│ 1 ┆ m ┆ 3 ┆ 5 ┆ o │
│ 1 ┆ n ┆ 3 ┆ 5 ┆ n │
│ 1 ┆ o ┆ 3 ┆ 1 ┆ m │
│ 2 ┆ m ┆ 4 ┆ 5 ┆ n │
│ 2 ┆ m ┆ 4 ┆ 5 ┆ n │
│ 2 ┆ n ┆ 4 ┆ 5 ┆ m │
│ 2 ┆ n ┆ 4 ┆ 5 ┆ m │
└─────┴──────┴──────┴─────┴──────────────┘
欠損データ
pandas では、列の dtype に応じて NaN
や None
の値を使用して欠損値を示します。さらに、pandas ではデフォルトの dtype またはオプションの nullable 配列を使用するかによって挙動が異なります。Polars では、すべてのデータ型に対して欠損データは null
値に対応します。
浮動小数点のカラムにおいて、Polars は NaN
値の使用を許可しています。これらの NaN
値は欠損データとは見なされず、特別な浮動小数点値として扱われます。
pandas では、欠損値を持つ整数列は、欠損値のために NaN
値を持つ浮動小数点列にキャストされます(オプションの null を許容する整数型の dtype を使用しない限り)。Polars では、整数列の欠損値は単に null
値であり、列は引き続き整数列のままです。
詳細については、欠損データ セクションを参照してください。
パイプの使用
pandas で一般的な使用方法は、pipe
を利用して DataFrame
に何らかの関数を適用することです。
このコーディングスタイルを Polars にそのまま適用するのは自然ではなく、最適ではないなクエリ計画につながります。
以下のスニペットは、pandas でよく見られるパターンを示しています。
def add_foo(df: pd.DataFrame) -> pd.DataFrame:
df["foo"] = ...
return df
def add_bar(df: pd.DataFrame) -> pd.DataFrame:
df["bar"] = ...
return df
def add_ham(df: pd.DataFrame) -> pd.DataFrame:
df["ham"] = ...
return df
(df
.pipe(add_foo)
.pipe(add_bar)
.pipe(add_ham)
)
Polars でこれを行うと、3つの with_columns
式を作成してしまい、
Polars に3つのパイプを順番に実行させることになり、並列処理は一切利用されません。
Polars で同様の抽象化を得る方法は、エクスプレッションを生成する関数を作成することです。 以下のスニペットでは、単一の式で実行される3つのエクスプレッションを作成し、これにより並列実行が可能になります。
def get_foo(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("foo")
def get_bar(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("bar")
def get_ham(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("ham")
# This single context will run all 3 expressions in parallel
df.with_columns(
get_ham("col_a"),
get_bar("col_b"),
get_foo("col_c"),
)
式を生成する関数内でスキーマが必要な場合、単一の pipe
を利用することができます:
from collections import OrderedDict
def get_foo(input_column: str, schema: OrderedDict) -> pl.Expr:
if "some_col" in schema:
# branch_a
...
else:
# branch b
...
def get_bar(input_column: str, schema: OrderedDict) -> pl.Expr:
if "some_col" in schema:
# branch_a
...
else:
# branch b
...
def get_ham(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("ham")
# Use pipe (just once) to get hold of the schema of the LazyFrame.
lf.pipe(lambda lf: lf.with_columns(
get_ham("col_a"),
get_bar("col_b", lf.schema),
get_foo("col_c", lf.schema),
)
エクスプレッションを返す関数を書くことのもう一つの利点は、これらの関数が組み合わせ可能であることです。 式は連鎖させたり部分適用することができ、設計の柔軟性が大幅に向上します。