【Polars】Pandasに代わるPythonの次世代表計算ライブラリの基本操作を解説

Polars はPythonの表計算ライブラリの1つです。Pythonの表計算ライブラリといえばPandasが広く知られており、使用されている方も多いと思います。ですが、パフォーマンスの面ではPolarsのほうが優れているという検証結果がいくつもあります。

今回は、Polarsに馴染みの無い方、これから使ってみようとしている方向けに、Polarsの特徴と基本的な使用方法について紹介します。

Polarsとは

PolarsとはRustで開発された表計算ライブラリで、PythonのほかR、NodeJSでも使用可能です。

Polars logo

主な特徴として、以下のようなものがあります。

  • 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※
※フィールドを抽出しています。

FactTable.csv

DimPhyscian.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.describe()の結果

ユニークな行数をカウント

引数を指定しない場合、全フィールドで完全一致しないレコードの件数が取得されます。

df.n_unique()

指定の列でユニークな値をカウントする場合、subsetを指定します。複数の列を指定する場合は、配列形式で指定します。

#指定列のユニークな行数をカウント
df.n_unique(subset=["Payment"])

行の抽出

1行の抽出

インデックス番号を指定することで、指定の1行を抽出することができます。

#行の抽出(1行)
df[0]
df[0]

スライサーでの抽出

Pandas同様、スライサーでの抽出も可能です。

#行の抽出(スライス)
df[2:4]
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").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()