Polars はPythonの表計算ライブラリの1つです。Pythonの表計算ライブラリといえばPandasが広く知られており、使用されている方も多いと思います。ですが、パフォーマンスの面ではPolarsのほうが優れているという検証結果がいくつもあります。
今回は、Polarsに馴染みの無い方、これから使ってみようとしている方向けに、Polarsの特徴と基本的な使用方法について紹介します。
Polarsとは
PolarsとはRustで開発された表計算ライブラリで、PythonのほかR、NodeJSでも使用可能です。
主な特徴として、以下のようなものがあります。
- Rustで開発されており、処理が高速
- ローカルファイル、クラウド、データベース等多様なデータソースとのI/Oに対応
- 直感的なコーディングで可読性を高める。内部ではクエリが最適化される
- ストリーミングAPIで、全てのデータをメモリに保持することなく処理が可能
- CPUコア間で処理不可を動的に分散することで、処理リソースを最大化する
- 内部ではApache Arrowを使用しており、コンピュータリソースの効率化および処理速度向上
データ構造
データ構造はPandasと同様で、Series、DataFrameの概念があります。一次元配列をSeriesとよび、一次元配列の集合をDataFrameと呼びます。DataFrameはいわゆる表形式のデータ構造をしており、各列は1つのSeriesとみることができます。
Pandasとの違いとして、列名は必ず文字列である必要があります(Pandasでは数値の列名も許容されます)。
Expression
PolarsにはExpressionという概念があります。簡潔に言うと、データ操作の流れを表現したオブジェクトです。
例えば以下のようなデータフレームを準備します。
df = pl.DataFrame(
{
"nrs": [1, 2, 3, None, 5],
"names": ["foo", "ham", "spam", "egg", None],
"random": np.random.rand(5),
"groups": ["A", "A", "B", "C", "B"],
}
)
print(df)
shape: (5, 4)
┌──────┬───────┬──────────┬────────┐
│ nrs ┆ names ┆ random ┆ groups │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ str │
╞══════╪═══════╪══════════╪════════╡
│ 1 ┆ foo ┆ 0.154163 ┆ A │
│ 2 ┆ ham ┆ 0.74005 ┆ A │
│ 3 ┆ spam ┆ 0.263315 ┆ B │
│ null ┆ egg ┆ 0.533739 ┆ C │
│ 5 ┆ null ┆ 0.014575 ┆ B │
└──────┴───────┴──────────┴────────┘
このデータフレームを加工して、以下のようなデータフレームを作成します。
df_numerical = df.select(
(pl.col("nrs") + 5).alias("nrs + 5"),
(pl.col("nrs") - 5).alias("nrs - 5"),
(pl.col("nrs") * pl.col("random")).alias("nrs * random"),
(pl.col("nrs") / pl.col("random")).alias("nrs / random"),
)
print(df_numerical)
shape: (5, 4)
┌─────────┬─────────┬──────────────┬──────────────┐
│ nrs + 5 ┆ nrs - 5 ┆ nrs * random ┆ nrs / random │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ f64 ┆ f64 │
╞═════════╪═════════╪══════════════╪══════════════╡
│ 6 ┆ -4 ┆ 0.154163 ┆ 6.486647 │
│ 7 ┆ -3 ┆ 1.480099 ┆ 2.702521 │
│ 8 ┆ -2 ┆ 0.789945 ┆ 11.393198 │
│ null ┆ null ┆ null ┆ null │
│ 10 ┆ 0 ┆ 0.072875 ┆ 343.054056 │
└─────────┴─────────┴──────────────┴──────────────┘
df_numericalではdfのデータに計算を施し、4列のデータを作成しています。polarsではこの4列の各処理がExpressionと表現されます。各Expressionでは、まず四則演算を実行し、.alias()で列名を指定するというデータ操作を行っています。
なお、Expressionの上位階層にある.select()はContext(式)と呼ばれ、ExpressionはContextに渡すデータ操作の流れを表現するオブジェクトとなります。
※概念的な部分はほどほどに、まずは使ってみることが重要です
このとき、各Expressionは並列で実行されます。
このような形で処理を並列化し、高速化できることがPolarsの強みです。
Polars基本操作
ここからは、Polars の基本的な操作についてご紹介します。Pandasと同様の使い方ができる処理もあれば、異なる処理もあるのでご注意ください。
前提
扱うデータ
今回は、以下のcsvファイルを使って操作を説明します。
https://www.kaggle.com/datasets/tomaslui/healthcare-dataset
FactTable.csv※
※フィールドを抽出しています。
DimPhyscian.csv
FactTable.csvのdimPhyscianPKはDimPhyscian.csvの外部キーです。
他、Excelファイル等も部分的に扱いますが、読込処理のみなので内容の説明は割愛します。
データ読込
csvファイルの読込
df = pl.read_csv("data/FactTable.csv")
Excelファイルの読込
Excelファイルの読込はPandas同様ですが、実行するにはfastexcel, pyarrowのインストールが必要です。
pl.read_excel("data/3MINDIA.xlsx")
JSONテキストの読込
#JSONデータの作成
from io import StringIO
json_str = '[{"foo":1,"bar":6},{"foo":2,"bar":7},{"foo":3,"bar":8}]'
pl.read_json(StringIO(json_str))
情報の取得
先頭N件の取得
以下は、先頭5件を取得する場合です。
df.head(5)
末尾N件の取得
以下は、末尾5件を取得する場合です。
df.tail(5)
要約統計量
データフレームの各列について、以下の情報を取得することができます。
- 件数
- Null件数
- 平均
- 標準偏差
- 最小値
- 四分位点
- 最大値
なお、戻り値は要約統計量のデータフレームです。
df.describe()
ユニークな行数をカウント
引数を指定しない場合、全フィールドで完全一致しないレコードの件数が取得されます。
df.n_unique()
指定の列でユニークな値をカウントする場合、subsetを指定します。複数の列を指定する場合は、配列形式で指定します。
#指定列のユニークな行数をカウント
df.n_unique(subset=["Payment"])
行の抽出
1行の抽出
インデックス番号を指定することで、指定の1行を抽出することができます。
#行の抽出(1行)
df[0]
スライサーでの抽出
Pandas同様、スライサーでの抽出も可能です。
#行の抽出(スライス)
df[2:4]
Expressionでの抽出(単一条件)
pl.col(“GrossCharge”)>0がExpressionに相当します。”GrossCharge”列が0以上という条件で演算した結果を、filter(Context)に渡しています。
df.filter(pl.col("GrossCharge")>0)
Expressionでの抽出(複数条件)
filterに複数条件を与える場合は、ExpressionをAnd(&)またはOr(|)で連結して渡します。
#AND条件
df.filter((pl.col("GrossCharge")>0) & (pl.col("CPTUnits")>0))
#OR条件
df.filter((pl.col("GrossCharge")>0) | (pl.col("CPTUnits")>0))
また、Expressionをカンマ区切りで渡すことも可能です。この場合、各ExpressionのAND条件と解釈されます。
#カンマ区切りはAND条件と判定される
df.filter(pl.col("GrossCharge")>0, pl.col("CPTUnits")>0)
列の抽出
get_column()
単一の列をSeriesで取得したい場合はget_columns()を使用します。
複数列の指定はできません。
df.get_column("GrossCharge")
select()
select()で単一列を指定した場合、1列のデータフレームとして返されます。この点が、get_column()との違いです。
df.select("GrossCharge")
また、リスト形式で渡すことで複数列の指定も可能です。
df.select(["GrossCharge", "Payment"])
行×列の抽出
行と列でそれぞれ条件を指定して抽出したい場合は、各処理を連結させて実行します。
#行の抽出 -> 列の抽出
df.filter(pl.col("GrossCharge")>5).select(["GrossCharge", "Payment"])
このとき、列の抽出を先に実行する場合は注意が必要です。抽出する列に、行の抽出に使用する列を必ず含める必要があるからです。上記の場合、”GrossCharge”は必ず指定する必要があります。
# "GrossCharge"は必ず抽出列に指定する必要がある
df.select(["GrossCharge", "Payment"]).filter(pl.col("GrossCharge")>5)
列→行の抽出は、上述の箇所にバグが入る可能性があるためあまりお勧めしません。行列複合的に抽出する場合は、行→列の順が無難だと思います。
新規列の追加
新規に列を追加する場合、まず追加するSeriesを別で作成し、with_columns()で追加します。
下記の場合、”GrossCharge2″列が新規で追加されます。
#追加するSeriesを作成
new_sr = (df.get_column("GrossCharge") * 2).alias("GrossCharge2")
#データフレームに追加
df.with_columns(new_sr)
結合
行方向(縦方向)の結合
行方向の結合はconcat()を使います。
concatでは、3つ以上のデータフレームをまとめて結合することができますが、各データフレームの列名が完全に一致している必要があります。
#データフレームのクローンを作成
df_same = df
#行方向への連結。結合元のデータフレーム同士は列数と列名が一致している必要あり
pl.concat([df, df_same])
列方向(横方向)の結合
列方向への結合はjoin()を使います。
以下ではhowで”left”を指定しており、左外部結合となります。この時、dfが左、df1が右に位置します。
howにはleftの他inner, right, full(完全外部結合)、cross(交差結合)等指定することができます。
df1 = pl.read_csv("data/DimPhyscian.csv")
df.join(df1, on="dimPhysicianPK", how="left")
GroupBy
データフレームを集約して処理を実施する場合はgroup_by()を使用します。
group_by()には単独の列および複数列の指定が可能です。複数列を指定した場合、指定した全列でグループを作成します。
group_by()では集約したオブジェクトが作成され、そこから集約関数を呼び出すことで、集約後の値を取得することができます。
集約関数は平均(mean)、件数(len)、最大(max)、min(最小)等様々用意されています。
#集約用のデータフレームを作成
df_group = df.join(df1, on="dimPhysicianPK", how="left")
# "ProviderSpecialty"ごとの平均
df_group.group_by("ProviderSpecialty").mean()
#各グループのデータ件数
df_group.group_by("ProviderSpecialty").len()
出力
csv出力
df.write_csv("output/out.csv")
Excel出力
Excelの出力にはxlsxwriterのインストールが必要です。
df.write_excel("output/out.xlsx")
JSON形式の出力
df.write_json()
ndjsonでの出力
df.write_ndjson()