LoginSignup
368
367

pandasから移行する人向け polars使用ガイド

Last updated at Posted at 2022-10-21

pandasから移行する人向け polars使用ガイド

polarsは、Pythonの表計算ライブラリです。Pythonではpandasがこの分野ですでに支配的となっていますが、polarsはパフォーマンス上pandasより優れているとされます。本記事はpandasからpolarsに移行する人にとりあえず知っておくべきいくつかの知識とユースケースを提供します。

polarsは更新が活発で、頻繁に新しい関数の実装やたまに仕様変更が行われています。都度、公式の最新のドキュメントを確認することをおすすめします。

本記事の内容はバージョン0.20.1 (2023/12/19)で確認しています。

基礎

polarsのデータ構造はpandasと同様です。一つの一次元配列をシリーズ(pl.Series)と呼びます。また、一つ以上のシリーズが集まってできた二次元配列をデータフレーム(pl.DataFrame)と呼びます。pandas同様、データフレームをテーブルとしてみたとき、それぞれのシリーズはに相当します。

なおpandasと違い、pl.DataFrameにはインデックス列は存在しません

polarsでは、列名は文字列でなければなりません。また、列名の重複を許可しません。なお、pandasでは列名もインデックスオブジェクトになっていますが、polarsにはインデックスオブジェクトというものは存在しないため、列名(DataFrame.columns)はただのリストで保持されています。

データ型

polarsのシリーズは、pandasのそれと同様、データ型(dtype)が存在します。以下はデータ型の例です。

polarsの型 pandas(Numpy)の類似する型 説明
pl.Int8 ~ pl.Int64 np.int8 ~ np.int64 整数
pl.UInt8 ~ pl.UInt64 np.uint8 ~ np.uint64 整数(符号なし)
pl.Float32 / pl.Float64 np.float32 / np.float64 浮動小数
pl.Boolean np.bool / pd.BooleanDtype 真偽値
pl.Utf8 pd.StringDtype 文字列
pl.Categorical pd.CategoricalDtype カテゴリー
pl.Enum pd.CategoricalDtype カテゴリー(事前定義)
pl.List - リスト(可変長)
pl.Array - リスト(固定長)
pl.Struct - 構造化配列
pl.Date - 日付
pl.Time - 時刻(日付無し)
pl.Datetime np.datetime64 日時
pl.Duration np.timedelta64 時間(時刻の差)
pl.Object np.object_ オブジェクト
  • pandasではデフォルトでは文字列をオブジェクトデータ型で扱うようになっています。それに対してpolarsには最初から文字列専用のデータ型が用意されています。
  • pandasで日付を扱う場合は一般的には時刻を0:00:00にした日時np.datetime64で代用しますが、polarsには日付のみを扱うpl.Dateが存在し、また時刻のみを扱うpl.Time、日付+時刻を扱うpl.Datetimeもそれぞれ存在します。
  • 型変換メソッド、つまりpandasの.astype()は、polarsでは.cast()です。

エクスプレッション(pl.Expr()

polarsには、データフレーム・シリーズとは別にエクスプレッションというクラスが存在します。エクスプレッションは「一連の操作の命令」だと思うとよいでしょう。

例えば、「"A"列を選択→3で割る→10より小さい値は2倍して大きい値は2で割る→列名を"AA"に変更(せよ)」という「一連の操作(の命令)」をオブジェクトとして持つことができます。このエクスプレッションオブジェクトを、データフレームやシリーズの特定のメソッドに与えることで実際にその操作が行われます。

このように一連の操作を指定してから実行することで、パフォーマンスの上昇が見込めます。

シリーズとエクスプレッションは原則として同じ名前の同じ機能のメソッドが用意されています。

ファイル読み書き

CSVファイルの読み込み

CSVファイルの読み込みはpandas同様read_csv()関数を使います。第一引数としてファイルパスを受け取り、データフレームを返します。

read_csv()関数の例
df = pl.read_csv("./example/data.csv")

pandasの関数とキーワードや指定方法が異なる引数が存在しますので注意してください。

引数 説明
has_header bool 1行目がヘッダかどうか。デフォルトはTrue
skip_rows int 先頭から何行読み飛ばすか。pandasと違いList[int]を渡すことはできません。
columns List[int | str] 使用する列。pandasのuse_cols
new_columns List[str] 新しい列名。ヘッダがないファイルを読み込んだ場合や、元の列名を無視したい場合に使います。
dtypes Mapping データ型。列数と同じ長さのリストや、列名とそれに対応するデータ型の辞書などが渡せます。
parse_dates bool pl.Datetimeにキャストするかどうか。pandasのように特定の列を選択することはできません。

次の引数はpandasの関数と同じように動作します。

引数 説明
sep str 列の区切り記号。デフォルトは','
encoding str エンコード。デフォルトは'utf8'

なお、polarsにはscan_csv()という関数もあります。受け取る引数はread_csv()関数とほぼ同じです。

pl.scan_csv(file)

scan_csv()の戻り値はDataFrameではなく、遅延評価を行うLazyFrameです。LazyFrame.collect()メソッド(daskの.compute()に相当)が実行されるまで計算が行われないという特徴があります。

他の配列から変換

pl.DataFrame()コンストラクタを使ってデータフレームを作成できます。このときschema引数を渡すと列名を設定できます。schema引数は列名のリスト、または{列名:データ型}の辞書で指定します(元データがすでに列名やデータ型を所持している場合はここで指定したもので上書きされます)。

rng = np.random.default_rng(0)
df = pl.DataFrame(rng.random((8, 4)), schema="ABCD")

df
# shape: (8, 4)
# ┌──────────┬──────────┬──────────┬──────────┐
# │ A        ┆ B        ┆ C        ┆ D        │
# │ ---      ┆ ---      ┆ ---      ┆ ---      │
# │ f64      ┆ f64      ┆ f64      ┆ f64      │
# ╞══════════╪══════════╪══════════╪══════════╡
# │ 0.636962 ┆ 0.269787 ┆ 0.040974 ┆ 0.016528 │
# │ 0.81327  ┆ 0.912756 ┆ 0.606636 ┆ 0.729497 │
# │ 0.543625 ┆ 0.935072 ┆ 0.815854 ┆ 0.002739 │
# │ 0.857404 ┆ 0.033586 ┆ 0.729655 ┆ 0.175656 │
# │ 0.863179 ┆ 0.541461 ┆ 0.299712 ┆ 0.422687 │
# │ 0.02832  ┆ 0.124283 ┆ 0.670624 ┆ 0.64719  │
# │ 0.615385 ┆ 0.383678 ┆ 0.99721  ┆ 0.980835 │
# │ 0.685542 ┆ 0.650459 ┆ 0.688447 ┆ 0.388921 │
# └──────────┴──────────┴──────────┴──────────┘

データとして{列名:配列}の辞書を渡すこともできます。配列は全て同じ長さである必要があります。なお、スカラー値を渡した場合はブロードキャストされます。

df = pl.DataFrame(
    {
        "Integer": [1, 2, 3, 4],
        "Float": np.array([1, 2, 3, 4], dtype=float),
        "Datetime": [datetime.datetime(2022, 4, 1)] * 4,
        "String": ["test", "train", "test", "train"],
    }
)

df
# shape: (4, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └─────────┴───────┴─────────────────────┴────────┘

データフレームのCSVファイル出力

データフレームをCSVファイルとして出力したい場合は、データフレームの.write_csv()メソッドを使います。なお、ヘッダーをファイルの1行目として出力しない場合はhas_header=Falseを指定します。

write_csv()関数の例
df.write_csv("./example/data.csv")

なお、polarsのCSVファイル出力関数は文字コードを指定できません(UTF8が強制されます)。宗教的理由でUTF8で保存することが禁じられている場合は、次のようにします。

Shift-JISで保存する例
with open("./example/data.csv", "w", encoding="sjis") as fw:
    fw.write(df.write_csv())

すなわち、.write_csv()メソッドに第一引数(書き込み先ファイルパス)を与えなかった場合、戻り値としてcsvの文字列データが返されますので、それをPython標準の方法でテキストファイルに書き込みます。

データフレームの視覚的確認

print(pl.DataFrame)

標準のprint()関数にデータフレームを渡すと、最初と最後の5行が表示されます。

print(pl.DataFrame({"A": np.arange(100)}))
# shape: (100, 1)
# ┌─────┐
# │ A   │
# │ --- │
# │ i32 │
# ╞═════╡
# │ 0   │
# │ 1   │
# │ 2   │
# │ 3   │
# │ ... │
# │ 96  │
# │ 97  │
# │ 98  │
# │ 99  │
# └─────┘

なお、最初と最後の行を抽出する、pandasと同様の.head().tail()メソッドも実装されています。

.glimpse()メソッド

.glimpse()メソッドでは、文字列として、各列の先頭の要素10個が表示されます。列数が多い場合、print()よりもこちらのほうが見やすいとされています。

print(df.glimpse())
# Rows: 4
# Columns: 4
# $ Integer           <i64> 1, 2, 3, 4
# $ Float             <f64> 1.0, 2.0, 3.0, 4.0
# $ Datetime <datetime[μs]> 2022-04-01 00:00:00, 2022-04-01 00:00:00, 2022-04-01 00:00:00, 2022-04-01 00:00:00
# $ String            <str> test, train, test, train

print(df) # 比較のため再掲
# shape: (4, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └─────────┴───────┴─────────────────────┴────────┘

.describe()メソッド

polarsでも、pandasとほぼ同様の.describe()メソッドを使用することができます。表示される情報は、データ数・欠損値の数・平均値・標準偏差・最小値・25%tile値・中央値・75%tile値・最大値です。

df.describe()
# shape: (9, 5)
# ┌────────────┬──────────┬──────────┬─────────────────────┬────────┐
# │ describe   ┆ Integer  ┆ Float    ┆ Datetime            ┆ String │
# │ ---        ┆ ---      ┆ ---      ┆ ---                 ┆ ---    │
# │ str        ┆ f64      ┆ f64      ┆ str                 ┆ str    │
# ╞════════════╪══════════╪══════════╪═════════════════════╪════════╡
# │ count      ┆ 4.0      ┆ 4.0      ┆ 4                   ┆ 4      │
# │ null_count ┆ 0.0      ┆ 0.0      ┆ 0                   ┆ 0      │
# │ mean       ┆ 2.5      ┆ 2.5      ┆ null                ┆ null   │
# │ std        ┆ 1.290994 ┆ 1.290994 ┆ null                ┆ null   │
# │ min        ┆ 1.0      ┆ 1.0      ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 25%        ┆ 2.0      ┆ 2.0      ┆ null                ┆ null   │
# │ 50%        ┆ 3.0      ┆ 3.0      ┆ null                ┆ null   │
# │ 75%        ┆ 3.0      ┆ 3.0      ┆ null                ┆ null   │
# │ max        ┆ 4.0      ┆ 4.0      ┆ 2022-04-01 00:00:00 ┆ train  │
# └────────────┴──────────┴──────────┴─────────────────────┴────────┘

.describe()メソッドの出力の一行目(データ数 count)は、バージョン0.19.19以前は欠損値を含めた数(=データフレームの行数、すなわちすべての列で同じ値)が表示されていましたが、バージョン0.20.0より欠損値を含めない数(すなわち列によって異なる可能性がある値)が表示されるようになりました。

行の抽出・フィルタリング・条件選択

[]を使った行の選択

行選択はdf[][]に整数を渡すことで行います(Python標準のリストと同様)。polarsにはインデックス列が存在しないため、pandasと違い.loc.ilocは存在しません。

# 一行選択
df[0]
# shape: (1, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# └─────────┴───────┴─────────────────────┴────────┘

# 行スライス
df[1:3]
# shape: (2, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# └─────────┴───────┴─────────────────────┴────────┘

.head().tail()

.head().tail()はpandasと同様、最初・最後の数行(デフォルトでは5行)を抽出します。

pl.DataFrame(np.arange(15)).head()
# shape: (5, 1)
# ┌──────────┐
# │ column_0 │
# │ ---      │
# │ i32      │
# ╞══════════╡
# │ 0        │
# │ 1        │
# │ 2        │
# │ 3        │
# │ 4        │
# └──────────┘

pl.DataFrame(np.arange(15)).tail(3)
# shape: (3, 1)
# ┌──────────┐
# │ column_0 │
# │ ---      │
# │ i32      │
# ╞══════════╡
# │ 12       │
# │ 13       │
# │ 14       │
# └──────────┘

行フィルタリング(.filter()

条件にもとづいて特定の行を抽出する場合は.filter()メソッドを用います。pandasのdf[df["column"] > 3]のような書き方はできません。

.filter()メソッドでは通常、エクスプレッション(pl.Expr())を渡すことでフィルタリングします。pandasの.query()メソッドにやや似てるといえるかもしれません。以下の例で使用しているpl.col()エクスプレッションは列選択を意味し、(単一列選択の場合は)シリーズと同様のメソッドをチェーンすることができます。

rng = np.random.default_rng(5)
df_num = pl.DataFrame(rng.integers(0, 10, (6, 3)), schema="ABC")

df_num
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 6   ┆ 8   ┆ 0   │
# │ 8   ┆ 4   ┆ 5   │
# │ 6   ┆ 2   ┆ 9   │
# │ 0   ┆ 2   ┆ 3   │
# │ 5   ┆ 4   ┆ 1   │
# │ 0   ┆ 0   ┆ 0   │
# └─────┴─────┴─────┘

df_num.filter(pl.col("A") > 5)
# shape: (3, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 6   ┆ 8   ┆ 0   │
# │ 8   ┆ 4   ┆ 5   │
# │ 6   ┆ 2   ┆ 9   │
# └─────┴─────┴─────┘

pandas同様、and条件は&で、or条件は|でつなぎます。&|は計算優先順位が高いので、括弧()の位置に注意しましょう。

df_num.filter((pl.col("A") < pl.col("B")) & (pl.col("C") < 5))
# shape: (2, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 6   ┆ 8   ┆ 0   │
# │ 0   ┆ 2   ┆ 3   │
# └─────┴─────┴─────┘

.filter()メソッドには引数を複数与えることができ、それらはand条件として解釈されます。

# 上の例と等価
df_num.filter(
    pl.col("A") < pl.col("B"),
    pl.col("C") < 5,
)
# shape: (2, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 6   ┆ 8   ┆ 0   │
# │ 0   ┆ 2   ┆ 3   │
# └─────┴─────┴─────┘

キーワード引数を与えるとpl.col(kwarg) == として解釈されます。

df_num.filter(B=2)  # `df_num.filter(pl.col("B") == 2)`と等価
# shape: (2, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 6   ┆ 2   ┆ 9   │
# │ 0   ┆ 2   ┆ 3   │
# └─────┴─────┴─────┘

df_num.filter(pl.col("A") < 1, B=2, C=3)
# shape: (1, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 0   ┆ 2   ┆ 3   │
# └─────┴─────┴─────┘

もちろん、データフレームの行数と同じ長さの真偽値配列を渡すこともできます。

df_num.filter([False, True, False, True, True, False])
# shape: (3, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 8   ┆ 4   ┆ 5   │
# │ 0   ┆ 2   ┆ 3   │
# │ 5   ┆ 4   ┆ 1   │
# └─────┴─────┴─────┘

リストに含まれるかどうかを判定するSeries.is_in()

シリーズの各値が、別のリストの中に含まれるかどうかを判定するには.is_in()メソッドを用います。pandasの.isin()メソッドに相当します。

df.get_column("Integer").is_in([2, 4, 6])
# shape: (4,)
# Series: 'Integer' [bool]
# [
#     false
#     true
#     false
#     true
# ]

df.filter(pl.col("Integer").is_in([2, 4, 6]))
# shape: (2, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └─────────┴───────┴─────────────────────┴────────┘

バージョン0.16.14以前のpolarsの.is_in()メソッドは、pandasと違い、セットやndarrayを渡すことはできませんでした。バージョン0.16.15以降はほとんどのイテラブルデータに対応しています。

ユニーク行・重複行

.unique()メソッドは、pandasの.drop_duplicates()に相当し、値が重複しているデータを削除することができます。

  • subset=引数:識別する列を指定します。デフォルトでは、全ての列の値が一致している行のみが削除されます。
  • keep=引数:デフォルトのanyは、重複行のうち予測不可能な いずれか1行 を残します。keep="first"を指定すると重複行のうち最初に出現する行が残されます。keep="last"を指定すると最後に出現する行が残ります。keep="none"を指定すると重複行をすべて削除します。
df_dup = pl.DataFrame(
    {
        "a": ["one", "one", "two", "two", "two", "three", "four"],
        "b": ["x", "y", "x", "y", "x", "x", "x"],
        "c": rng.random(7),
    }
)

df_dup.unique(subset=["a", "b"])
# shape: (6, 3)
# ┌───────┬─────┬──────────┐
# │ a     ┆ b   ┆ c        │
# │ ---   ┆ --- ┆ ---      │
# │ str   ┆ str ┆ f64      │
# ╞═══════╪═════╪══════════╡
# │ one   ┆ x   ┆ 0.999176 │
# │ one   ┆ y   ┆ 0.652369 │
# │ two   ┆ x   ┆ 0.23451  │
# │ two   ┆ y   ┆ 0.434948 │
# │ three ┆ x   ┆ 0.897678 │
# │ four  ┆ x   ┆ 0.844231 │
# └───────┴─────┴──────────┘

df_dup.unique(subset=["a"])
# shape: (4, 3)
# ┌───────┬─────┬──────────┐
# │ a     ┆ b   ┆ c        │
# │ ---   ┆ --- ┆ ---      │
# │ str   ┆ str ┆ f64      │
# ╞═══════╪═════╪══════════╡
# │ one   ┆ x   ┆ 0.999176 │
# │ two   ┆ x   ┆ 0.23451  │
# │ three ┆ x   ┆ 0.897678 │
# │ four  ┆ x   ┆ 0.844231 │
# └───────┴─────┴──────────┘

df_dup.unique(subset=["a"], keep="last")
# shape: (4, 3)
# ┌───────┬─────┬──────────┐
# │ a     ┆ b   ┆ c        │
# │ ---   ┆ --- ┆ ---      │
# │ str   ┆ str ┆ f64      │
# ╞═══════╪═════╪══════════╡
# │ one   ┆ y   ┆ 0.652369 │
# │ two   ┆ x   ┆ 0.974186 │
# │ three ┆ x   ┆ 0.897678 │
# │ four  ┆ x   ┆ 0.844231 │
# └───────┴─────┴──────────┘

df_dup.unique(subset=["a"], keep="none")
# shape: (2, 3)
# ┌───────┬─────┬──────────┐
# │ a     ┆ b   ┆ c        │
# │ ---   ┆ --- ┆ ---      │
# │ str   ┆ str ┆ f64      │
# ╞═══════╪═════╪══════════╡
# │ three ┆ x   ┆ 0.886226 │
# │ four  ┆ x   ┆ 0.228849 │
# └───────┴─────┴──────────┘

バージョン0.17.0以降、パフォーマンス向上のため.unique()メソッドの出力はデータフレームの行の順序を保持しない(すなわち行がシャッフルされた状態で返される)ようになりました。.unique()適用後もデータフレームの順序を保持したい場合はmaintain_order=Trueを指定してください。

なお、デフォルトのmaintain_order=Falseの状態でも、keep="first"keep="last"は期待した通りに動作します。

重複するかどうかの真偽値は.is_duplicated()で確認でき、逆にユニークかどうかは.is_unique()で確認できますが、subset=keep=引数を渡すことはできません。

df_dup.is_duplicated()
# shape: (7,)
# Series: '' [bool]
# [
#     false
#     false
#     false
#     false
#     false
#     false
#     false
# ]

df_dup.is_unique()
# shape: (7,)
# Series: '' [bool]
# [
#     true
#     true
#     true
#     true
#     true
#     true
#     true
# ]

先に列を絞ってから.is_duplicated()を適用することで、subset引数と同じ効果が得られます。

df_dup.get_column("a").is_duplicated()
# shape: (7,)
# Series: 'a' [bool]
# [
#     true
#     true
#     true
#     true
#     true
#     false
#     false
# ]

列の選択と追加

.get_column()を使ったシリーズの選択

データフレーム内の特定のシリーズ(1列)を選択する場合は、.get_column()メソッドに列名を渡します(pandasのようにdf[col]と書くことも可能です)。

df.get_column("Integer")
# shape: (4,)
# Series: 'Integer' [i64]
# [
#     1
#     2
#     3
#     4
# ]

.get_columns()を使ったシリーズの選択

.get_columns()は、データフレームの全ての列を分解し、シリーズのリストに変換します。これを用いると、「左からX番目の列」(Xは0はじまり)のシリーズを取得することができます。

type(df.get_columns())
# list

df.get_columns()[1]
# shape: (4,)
# Series: 'Float' [f64]
# [
#     1.0
#     2.0
#     3.0
#     4.0
# ]

なお、.get_column_index()メソッドに列名を渡すと、その列が左から何番目にあるかを教えてくれます。

df.get_column_index("Float")
# 1

.get_column_index()メソッドは、バージョン0.19.13までは.find_idx_by_name()という名前でした。

.select()を使った列選択

データフレーム内の(1つ以上の)特定の列を選択する場合は、.select()メソッドを用います。

.get_column()メソッドと違い、列名を一つ渡した場合の戻り値は、(シリーズではなく)1列のデータフレームです。

df.select("Integer")
# shape: (4, 1)
# ┌─────────┐
# │ Integer │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 1       │
# │ 2       │
# │ 3       │
# │ 4       │
# └─────────┘

列名を複数渡した場合、または列名のリストを渡した場合、2列以上のデータフレームになります。

df.select("Integer", "Float") 
# shape: (4, 2)
# ┌─────────┬───────┐
# │ Integer ┆ Float │
# │ ---     ┆ ---   │
# │ i64     ┆ f64   │
# ╞═════════╪═══════╡
# │ 1       ┆ 1.0   │
# │ 2       ┆ 2.0   │
# │ 3       ┆ 3.0   │
# │ 4       ┆ 4.0   │
# └─────────┴───────┘

バージョン0.16.2以前は、一列指定は.select()に直接列名を渡し、複数列指定の場合は.select()列名のリスト を渡すとされていましたが、バージョン0.16.3から列名を複数渡すことができるようになりました。

# バージョン0.16.2以前の記法(以降のバージョンでも可能)
df.select(["Integer", "Float"])

# バージョン0.16.3以降の記法
df.select("Integer", "Float")

複数のエクスプレッションを渡すこともできます。スカラー値が戻り値になるようなエクスプレッションはブロードキャストされます。

df.select(pl.col("Integer"), pl.col("Float"))
# shape: (4, 2)
# ┌─────────┬───────┐
# │ Integer ┆ Float │
# │ ---     ┆ ---   │
# │ i64     ┆ f64   │
# ╞═════════╪═══════╡
# │ 1       ┆ 1.0   │
# │ 2       ┆ 2.0   │
# │ 3       ┆ 3.0   │
# │ 4       ┆ 4.0   │
# └─────────┴───────┘

df.select(pl.col("Integer")*2, pl.col("Float").sum())
# shape: (4, 2)
# ┌─────────┬───────┐
# │ Integer ┆ Float │
# │ ---     ┆ ---   │
# │ i64     ┆ f64   │
# ╞═════════╪═══════╡
# │ 2       ┆ 10.0  │
# │ 4       ┆ 10.0  │
# │ 6       ┆ 10.0  │
# │ 8       ┆ 10.0  │
# └─────────┴───────┘

列の追加

データフレームに列を追加する場合は.with_columns()を使います(pandasの.assign()に似ています)。

基本的に、既存の列に特定の操作を行うことで新しい列をつくる場合は、シリーズ操作ではなくエクスプレッションを使うことを推奨します。例えば、以下の二つの操作は結果は同じですが、後者のエクスプレッションを用いた操作の方がメモリ・計算量の観点からは効率が良いです(なお、以下の例で用いている.alias()は選択された列の列名の変更を行うメソッドです)。

# 新しいシリーズの作成を伴う操作
new_seires = (df.get_column("Integer") * 2).alias("Integer2")
df.with_columns(new_seires)
# shape: (4, 5)
# ┌─────────┬───────┬─────────────────────┬────────┬──────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String ┆ Integer2 │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    ┆ ---      │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    ┆ i64      │
# ╞═════════╪═══════╪═════════════════════╪════════╪══════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 2        │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 4        │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 6        │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 8        │
# └─────────┴───────┴─────────────────────┴────────┴──────────┘

# エクスプレッションによる操作
new_column = (pl.col("Integer") * 2).alias("Integer2")
df.with_columns(new_column)
# shape: (4, 5)
# ┌─────────┬───────┬─────────────────────┬────────┬──────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String ┆ Integer2 │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    ┆ ---      │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    ┆ i64      │
# ╞═════════╪═══════╪═════════════════════╪════════╪══════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 2        │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 4        │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 6        │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 8        │
# └─────────┴───────┴─────────────────────┴────────┴──────────┘

既存の列名と同名の列を追加すると、追加ではなく更新になります。

df.with_columns(pl.col("Integer") * 100)
# shape: (4, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 100     ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 200     ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 300     ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 400     ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └─────────┴───────┴─────────────────────┴────────┘

.with_columns()にエクスプレッションを複数渡すと、一度に複数の列を追加できます。

df.with_columns(
    pl.sum("Integer").alias("Integer2"), (pl.col("Float") * 100).alias("Arange")
)
# shape: (4, 6)
# ┌─────────┬───────┬─────────────────────┬────────┬──────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String ┆ Integer2 ┆ Arange │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    ┆ ---      ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    ┆ i64      ┆ f64    │
# ╞═════════╪═══════╪═════════════════════╪════════╪══════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 10       ┆ 100.0  │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 10       ┆ 200.0  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 10       ┆ 300.0  │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 10       ┆ 400.0  │
# └─────────┴───────┴─────────────────────┴────────┴──────────┴────────┘

バージョン0.16.2以前は、.with_columns()列(エクスプレッションやシリーズ)のリスト を渡すとされていましたが、バージョン0.16.3からそれらを直接、複数の引数として渡すことができるようになりました。

# バージョン0.16.2以前の記法(以降のバージョンでも可能)
df.with_columns(
    [pl.sum("Integer").alias("Integer2"), (pl.col("Float") * 100).alias("Arange")]
)

# バージョン0.16.3以降の記法
df.with_columns(
    pl.sum("Integer").alias("Integer2"), (pl.col("Float") * 100).alias("Arange")
)

pandasの.assign()と同様に、新しい列名をキーワード引数として指定することもできます。

df.with_columns(
    Integer2=pl.sum("Integer"),
    Arange=pl.col("Float") * 100
)
# shape: (4, 6)
# ┌─────────┬───────┬─────────────────────┬────────┬──────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String ┆ Integer2 ┆ Arange │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    ┆ ---      ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    ┆ i64      ┆ f64    │
# ╞═════════╪═══════╪═════════════════════╪════════╪══════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 10       ┆ 100.0  │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 10       ┆ 200.0  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   ┆ 10       ┆ 300.0  │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  ┆ 10       ┆ 400.0  │
# └─────────┴───────┴─────────────────────┴────────┴──────────┴────────┘

.with_row_count()メソッドは左端に0始まりの整数列「row_nr」(列名はname=引数で指定可能)を追加します。感覚的にpandasの.reset_index()に似ているかもしれません。

df.with_row_count()
# shape: (4, 5)
# ┌────────┬─────────┬───────┬─────────────────────┬────────┐
# │ row_nr ┆ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---    ┆ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ u32    ┆ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞════════╪═════════╪═══════╪═════════════════════╪════════╡
# │ 0      ┆ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 1      ┆ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 2      ┆ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 3      ┆ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └────────┴─────────┴───────┴─────────────────────┴────────┘

なお.select()でも列の追加は可能です。次の例で使用されているpl.arange()は、np.arange()pd.RangeIndex()に相当する、等差数列を作成するエクスプレッションです。

df.select(pl.arange(0, df.height).alias("row_nr"), pl.col("*"))
# shape: (4, 5)
# ┌────────┬─────────┬───────┬─────────────────────┬────────┐
# │ row_nr ┆ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---    ┆ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64    ┆ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞════════╪═════════╪═══════╪═════════════════════╪════════╡
# │ 0      ┆ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 1      ┆ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 2      ┆ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 3      ┆ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └────────┴─────────┴───────┴─────────────────────┴────────┘

基礎的な操作と演算

四則演算

スカラー値との四則演算はNumPyやpandasと同様に動作します。

rng = np.random.default_rng(0)
df_num = pl.DataFrame(rng.integers(0, 10, (6, 3)), schema="ABC")

df_num
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 8   ┆ 6   ┆ 5   │
# │ 2   ┆ 3   ┆ 0   │
# │ 0   ┆ 0   ┆ 1   │
# │ 8   ┆ 6   ┆ 9   │
# │ 5   ┆ 6   ┆ 9   │
# │ 7   ┆ 6   ┆ 5   │
# └─────┴─────┴─────┘

df_num + 2
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 10  ┆ 8   ┆ 7   │
# │ 4   ┆ 5   ┆ 2   │
# │ 2   ┆ 2   ┆ 3   │
# │ 10  ┆ 8   ┆ 11  │
# │ 7   ┆ 8   ┆ 11  │
# │ 9   ┆ 8   ┆ 7   │
# └─────┴─────┴─────┘

df_num * 2
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 16  ┆ 12  ┆ 10  │
# │ 4   ┆ 6   ┆ 0   │
# │ 0   ┆ 0   ┆ 2   │
# │ 16  ┆ 12  ┆ 18  │
# │ 10  ┆ 12  ┆ 18  │
# │ 14  ┆ 12  ┆ 10  │
# └─────┴─────┴─────┘

べき乗**はシリーズに対してのみ動作します。データフレームのすべての要素にべき乗計算をしたい場合は.select()メソッドとエクスプレッションを使います。

df_num.get_column("A") ** 2
# shape: (6,)
# Series: 'A' [f64]
# [
#     64.0
#     4.0
#     0.0
#     64.0
#     25.0
#     49.0
# ]

df_num.select(pl.all().pow(2))
# shape: (6, 3)
# ┌──────┬──────┬──────┐
# │ A    ┆ B    ┆ C    │
# │ ---  ┆ ---  ┆ ---  │
# │ f64  ┆ f64  ┆ f64  │
# ╞══════╪══════╪══════╡
# │ 64.0 ┆ 36.0 ┆ 25.0 │
# │ 4.0  ┆ 9.0  ┆ 0.0  │
# │ 0.0  ┆ 0.0  ┆ 1.0  │
# │ 64.0 ┆ 36.0 ┆ 81.0 │
# │ 25.0 ┆ 36.0 ┆ 81.0 │
# │ 49.0 ┆ 36.0 ┆ 25.0 │
# └──────┴──────┴──────┘

同じ長さ(行数)であれば、データフレームとシリーズ、シリーズ同士の計算が可能です。

df_num + df_num.get_column("B")
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 14  ┆ 12  ┆ 11  │
# │ 5   ┆ 6   ┆ 3   │
# │ 0   ┆ 0   ┆ 1   │
# │ 14  ┆ 12  ┆ 15  │
# │ 11  ┆ 12  ┆ 15  │
# │ 13  ┆ 12  ┆ 11  │
# └─────┴─────┴─────┘

df_num.get_column("A") + df_num.get_column("B")
# shape: (6,)
# Series: 'A' [i64]
# [
#     14
#     5
#     0
#     14
#     11
#     13
# ]

比較演算

データフレームに対して直接比較演算はできません(.select()メソッドとエクスプレッションを使います)。シリーズは直接数値と比較可能です。

df_num.get_column("A") == 0
# shape: (6,)
# Series: 'A' [bool]
# [
#     false
#     false
#     true
#     false
#     false
#     false
# ]

df_num.get_column("A") > 2
# shape: (6,)
# Series: 'A' [bool]
# [
#     true
#     false
#     false
#     true
#     true
#     true
# ]

同じ長さのシリーズ同士を比較できます。

df_num.get_column("A") > df_num.get_column("B")
# shape: (6,)
# Series: '' [bool]
# [
#     true
#     false
#     false
#     true
#     false
#     true
# ]

等価比較==は数値以外でも可能です。

pl.Series(["foo", "bar", "baz"]) == pl.Series(["foo", "bar", "qux"])
# shape: (3,)
# Series: '' [bool]
# [
#     true
#     true
#     false
# ]

集計関数

.sum().mean()等の関数は列(垂直)方向に適用できます。

df_num.sum()
# shape: (1, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 30  ┆ 27  ┆ 29  │
# └─────┴─────┴─────┘

df_num.mean()
# shape: (1, 3)
# ┌─────┬─────┬──────────┐
# │ A   ┆ B   ┆ C        │
# │ --- ┆ --- ┆ ---      │
# │ f64 ┆ f64 ┆ f64      │
# ╞═════╪═════╪══════════╡
# │ 5.0 ┆ 4.5 ┆ 4.833333 │
# └─────┴─────┴──────────┘

df_num.quantile(0.5)
# shape: (1, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ f64 ┆ f64 ┆ f64 │
# ╞═════╪═════╪═════╡
# │ 7.0 ┆ 6.0 ┆ 5.0 │
# └─────┴─────┴─────┘

なお、行(水平)方向の集約は.sum_horizontal()等の“_horizontal”メソッドを使うか、より柔軟に複雑な計算を行う場合は.fold()メソッドを使います。後者はおおむねPython標準のfunctools.reduce()に対応します。

df_num.fold(lambda a, b: a+b)
# shape: (6,)
# Series: 'A' [i64]
# [
#     19
#     5
#     1
#     23
#     20
#     18
# ]

# 以下に等しい
from functools import reduce
reduce(lambda a, b: a+b, df_num)

出現回数を集計する.value_counts()

シリーズの.value_counts()はpandasと同様に扱うことができます。なおデータフレームには.value_counts()メソッドはありません(同等の結果は.groupby(pl.all()).count()で得ることができます)。

df_num["A"].value_counts()
# shape: (5, 2)
# ┌─────┬───────┐
# │ A   ┆ count │
# │ --- ┆ ---   │
# │ i64 ┆ u32   │
# ╞═════╪═══════╡
# │ 2   ┆ 1     │
# │ 8   ┆ 2     │
# │ 0   ┆ 1     │
# │ 5   ┆ 1     │
# │ 7   ┆ 1     │
# └─────┴───────┘

結果のデータフレームはどの列によってもソートされていません。sort=Trueを指定すると、出現回数多い順でソートされます(値の列でソート、または出現回数少ない順でソートしたい場合は.sort()メソッドを続けましょう)。

df_num["A"].value_counts(sort=True)
# shape: (5, 2)
# ┌─────┬───────┐
# │ A   ┆ count │
# │ --- ┆ ---   │
# │ i64 ┆ u32   │
# ╞═════╪═══════╡
# │ 8   ┆ 2     │
# │ 2   ┆ 1     │
# │ 0   ┆ 1     │
# │ 5   ┆ 1     │
# │ 7   ┆ 1     │
# └─────┴───────┘

なお、エクスプレッションに対する.value_counts()は、構造化配列を返します。

df_num.select(pl.col("A").value_counts())
# shape: (5, 1)
# ┌───────────┐
# │ A         │
# │ ---       │
# │ struct[2] │
# ╞═══════════╡
# │ {0,1}     │
# │ {8,2}     │
# │ {5,1}     │
# │ {7,1}     │
# │ {2,1}     │
# └───────────┘

列操作:エクスプレッションの主な使い方

シリーズ(場合によってはデータフレーム)になんらかの操作をする場合はエクスプレッションを与えたほうが便利です。

列選択・指定:pl.col()

列選択はpl.col()に列名を渡すことで行います。

df = pl.DataFrame(
    {
        "Integer": [1, 2, 3, 4],
        "Float": np.array([1, 2, 3, 4], dtype=float),
        "Datetime": [datetime.datetime(2022, 4, 1)] * 4,
        "String": ["test", "train", "test", "train"],
    }
)

df.select(pl.col("String"))
# shape: (4, 1)
# ┌────────┐
# │ String │
# │ ---    │
# │ str    │
# ╞════════╡
# │ test   │
# │ train  │
# │ test   │
# │ train  │
# └────────┘

列名を複数渡すと、複数列をまとめて選択できます。

df.select(pl.col("String", "Integer"))
# shape: (4, 2)
# ┌────────┬─────────┐
# │ String ┆ Integer │
# │ ---    ┆ ---     │
# │ str    ┆ i64     │
# ╞════════╪═════════╡
# │ test   ┆ 1       │
# │ train  ┆ 2       │
# │ test   ┆ 3       │
# │ train  ┆ 4       │
# └────────┴─────────┘

バージョン0.16.6以前は、複数列選択する場合はpl.col()列名のリスト を渡すとされていましたが、バージョン0.16.7から直接列名を複数渡すことができるようになりました。

# バージョン0.16.6以前の記法(以降のバージョンでも可能)
pl.col(["String", "Integer"])

# バージョン0.16.7以降の記法
pl.col("String", "Integer")

列名を"^ ... $"で囲むと正規表現が使えます。

df.select(pl.col(r"^[DS].+$"))
# shape: (4, 2)
# ┌─────────────────────┬────────┐
# │ Datetime            ┆ String │
# │ ---                 ┆ ---    │
# │ datetime[μs]        ┆ str    │
# ╞═════════════════════╪════════╡
# │ 2022-04-01 00:00:00 ┆ test   │
# │ 2022-04-01 00:00:00 ┆ train  │
# │ 2022-04-01 00:00:00 ┆ test   │
# │ 2022-04-01 00:00:00 ┆ train  │
# └─────────────────────┴────────┘

特定のデータ型の列を選択することもできます。

df.select(pl.col(pl.Int64))
# shape: (4, 1)
# ┌─────────┐
# │ Integer │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 1       │
# │ 2       │
# │ 3       │
# │ 4       │
# └─────────┘

pl.all()、またはpl.col("*")ですべての列を選択します。

df.select(pl.all())
# shape: (4, 4)
# ┌─────────┬───────┬─────────────────────┬────────┐
# │ Integer ┆ Float ┆ Datetime            ┆ String │
# │ ---     ┆ ---   ┆ ---                 ┆ ---    │
# │ i64     ┆ f64   ┆ datetime[μs]        ┆ str    │
# ╞═════════╪═══════╪═════════════════════╪════════╡
# │ 1       ┆ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 2       ┆ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 3       ┆ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 4       ┆ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └─────────┴───────┴─────────────────────┴────────┘

df.select(pl.col("*"))
# (同じ結果)

pl.exclude()は指定した列以外のすべてを選択します。pl.col()と同様にリストや正規表現もつかうことができます。

df.select(pl.exclude("Integer"))
# shape: (4, 3)
# ┌───────┬─────────────────────┬────────┐
# │ Float ┆ Datetime            ┆ String │
# │ ---   ┆ ---                 ┆ ---    │
# │ f64   ┆ datetime[μs]        ┆ str    │
# ╞═══════╪═════════════════════╪════════╡
# │ 1.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 2.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# │ 3.0   ┆ 2022-04-01 00:00:00 ┆ test   │
# │ 4.0   ┆ 2022-04-01 00:00:00 ┆ train  │
# └───────┴─────────────────────┴────────┘

また、pl.selectorsを使うとさらに複雑な条件を直感的に指定できます。

列名の変更

列名を任意の文字列に変更する場合は.alias()を使います。polarsでは列名の重複を許さないのでよく使います。.alias()pl.col()で二列以上選択している場合は使用できません。

df.select(pl.col("Integer").alias("ABC"), pl.col("Integer").alias("DEF"))
# shape: (4, 2)
# ┌─────┬─────┐
# │ ABC ┆ DEF │
# │ --- ┆ --- │
# │ i64 ┆ i64 │
# ╞═════╪═════╡
# │ 1   ┆ 1   │
# │ 2   ┆ 2   │
# │ 3   ┆ 3   │
# │ 4   ┆ 4   │
# └─────┴─────┘

.name.prefix().name.suffix()で列名の前後に任意の文字列をつなげることができます。

df.select(
    pl.col(["Integer", "Float"]).name.prefix("Col_"),
    pl.col("String").name.suffix("_col"),
)
# shape: (4, 3)
# ┌─────────────┬───────────┬────────────┐
# │ Col_Integer ┆ Col_Float ┆ String_col │
# │ ---         ┆ ---       ┆ ---        │
# │ i64         ┆ f64       ┆ str        │
# ╞═════════════╪═══════════╪════════════╡
# │ 1           ┆ 1.0       ┆ test       │
# │ 2           ┆ 2.0       ┆ train      │
# │ 3           ┆ 3.0       ┆ test       │
# │ 4           ┆ 4.0       ┆ train      │
# └─────────────┴───────────┴────────────┘

バージョン0.19.11以前は、Expr.name.prefix()/Expr.name.suffix()
はそれぞれExpr.prefix()/Expr.suffix()でした。

ソート・並び替え

ソートは.sort()を使います。

df_unsort = pl.DataFrame(
    {
        "col1": ["A", "A", "B", None, "D", "C"],
        "col2": [2, 1, 9, 8, 7, 4],
        "col3": [0, 1, 9, 4, 2, 3],
        "col4": ["a", "B", "c", "D", "e", "F"],
    }
)
# shape: (6, 4)
# ┌──────┬──────┬──────┬──────┐
# │ col1 ┆ col2 ┆ col3 ┆ col4 │
# │ ---  ┆ ---  ┆ ---  ┆ ---  │
# │ str  ┆ i64  ┆ i64  ┆ str  │
# ╞══════╪══════╪══════╪══════╡
# │ A    ┆ 2    ┆ 0    ┆ a    │
# │ A    ┆ 1    ┆ 1    ┆ B    │
# │ B    ┆ 9    ┆ 9    ┆ c    │
# │ null ┆ 8    ┆ 4    ┆ D    │
# │ D    ┆ 7    ┆ 2    ┆ e    │
# │ C    ┆ 4    ┆ 3    ┆ F    │
# └──────┴──────┴──────┴──────┘

df_unsort.select(pl.col("col1").sort())
# shape: (6, 1)
# ┌──────┐
# │ col1 │
# │ ---  │
# │ str  │
# ╞══════╡
# │ null │
# │ A    │
# │ A    │
# │ B    │
# │ C    │
# │ D    │
# └──────┘

pl.col()等で複数列選択していた場合でも、ソートは 列ごとに独立して 行われるので注意してください。

df_unsort.select(pl.col("col1", "col2").sort())
# shape: (6, 2)
# ┌──────┬──────┐
# │ col1 ┆ col2 │
# │ ---  ┆ ---  │
# │ str  ┆ i64  │
# ╞══════╪══════╡
# │ null ┆ 1    │
# │ A    ┆ 2    │
# │ A    ┆ 4    │
# │ B    ┆ 7    │
# │ C    ┆ 8    │
# │ D    ┆ 9    │
# └──────┴──────┘

特定の列に沿って複数列をソートする(各行内を維持してソートする)には.sort_by()を使用します。キーを複数指定することもできます。

df_unsort.select(pl.col("col1", "col2").sort_by("col1"))
# shape: (6, 2)
# ┌──────┬──────┐
# │ col1 ┆ col2 │
# │ ---  ┆ ---  │
# │ str  ┆ i64  │
# ╞══════╪══════╡
# │ null ┆ 8    │
# │ A    ┆ 2    │
# │ A    ┆ 1    │
# │ B    ┆ 9    │
# │ C    ┆ 4    │
# │ D    ┆ 7    │
# └──────┴──────┘

df_unsort.select(pl.col("col3").sort_by("col1", "col2"))
# shape: (6, 1)
# ┌──────┐
# │ col3 │
# │ ---  │
# │ i64  │
# ╞══════╡
# │ 4    │
# │ 1    │
# │ 0    │
# │ 9    │
# │ 3    │
# │ 2    │
# └──────┘

.sort()はデフォルトでは昇順ソートです。descending=Trueを指定すると降順になります。昇順・降順にかかわらず 欠損値は先頭 に並べられます。欠損値を最後に並べるにはnulls_last=Trueを指定します。

df_unsort.select(
    pl.col("col1").sort().alias("default"),
    pl.col("col1").sort(descending=True).alias("desc"),
    pl.col("col1").sort(nulls_last=True).alias("nlast"),
    pl.col("col1").sort(descending=True, nulls_last=True).alias("desc_nlast"),
)
# shape: (6, 4)
# ┌─────────┬──────┬───────┬────────────┐
# │ default ┆ desc ┆ nlast ┆ desc_nlast │
# │ ---     ┆ ---  ┆ ---   ┆ ---        │
# │ str     ┆ str  ┆ str   ┆ str        │
# ╞═════════╪══════╪═══════╪════════════╡
# │ null    ┆ null ┆ A     ┆ D          │
# │ A       ┆ D    ┆ A     ┆ C          │
# │ A       ┆ C    ┆ B     ┆ B          │
# │ B       ┆ B    ┆ C     ┆ A          │
# │ C       ┆ A    ┆ D     ┆ A          │
# │ D       ┆ A    ┆ null  ┆ null       │
# └─────────┴──────┴───────┴────────────┘

.reverse()は、列を上下逆に並び替えます(つまりスライス記法でいう[::-1])。

df_unsort.select(pl.col("col4").reverse())
# shape: (6, 1)
# ┌──────┐
# │ col4 │
# │ ---  │
# │ str  │
# ╞══════╡
# │ F    │
# │ e    │
# │ D    │
# │ c    │
# │ B    │
# │ a    │
# └──────┘

集計計算

列選択後、.sum()で総和を計算します。

df.select(pl.col("Integer").sum())
# shape: (1, 1)
# ┌─────────┐
# │ Integer │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 10      │
# └─────────┘

pl.col(列名).sum()は、省略してpl.sum(列名)と書くこともできます。

df.select(pl.sum("Integer"))
# shape: (1, 1)
# ┌─────────┐
# │ Integer │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 10      │
# └─────────┘

.sum()以外にも様々な関数があります。

df.select(
    pl.count("Integer").alias("count"),
    pl.n_unique("Integer").alias("n_unique"),
    pl.max("Integer").alias("max"),
    pl.min("Integer").alias("min"),
    pl.mean("Integer").alias("mean"),
    pl.median("Integer").alias("median"),
    pl.quantile("Integer", 0.5).alias("50%tile"),
    pl.std("Integer").alias("std"),
    pl.var("Integer").alias("var"),
)
# shape: (1, 9)
# ┌───────┬──────────┬─────┬─────┬─────┬────────┬─────────┬──────────┬──────────┐
# │ count ┆ n_unique ┆ max ┆ min ┆ ... ┆ median ┆ 50%tile ┆ std      ┆ var      │
# │ ---   ┆ ---      ┆ --- ┆ --- ┆     ┆ ---    ┆ ---     ┆ ---      ┆ ---      │
# │ u32   ┆ u32      ┆ i64 ┆ i64 ┆     ┆ f64    ┆ f64     ┆ f64      ┆ f64      │
# ╞═══════╪══════════╪═════╪═════╪═════╪════════╪═════════╪══════════╪══════════╡
# │ 4     ┆ 4        ┆ 4   ┆ 1   ┆ ... ┆ 2.5    ┆ 3.0     ┆ 1.290994 ┆ 1.666667 │
# └───────┴──────────┴─────┴─────┴─────┴────────┴─────────┴──────────┴──────────┘

バージョン0.18.15以前では、pl.col(複数列).sum()と、pl.sum([複数列])は集計方向が異なるので注意してください(.sum()以外の各種集計関数も同様)。

  • pl.col(複数列).sum()およびpl.sum(複数列)は列(垂直)方向を集約します。
  • pl.sum([複数列])は行(水平)方向を集約します。
バージョン0.18.15以前の挙動
df.select(pl.col("Integer", "Float").sum())
df.select(pl.sum("Integer", "Float"))
# shape: (1, 2)
# ┌─────────┬───────┐
# │ Integer ┆ Float │
# │ ---     ┆ ---   │
# │ i64     ┆ f64   │
# ╞═════════╪═══════╡
# │ 10      ┆ 10.0  │
# └─────────┴───────┘

df.select(pl.sum(["Integer", "Float"]))
# shape: (4, 1)
# ┌─────┐
# │ sum │
# │ --- │
# │ f64 │
# ╞═════╡
# │ 2.0 │
# │ 4.0 │
# │ 6.0 │
# │ 8.0 │
# └─────┘

バージョン0.19.0以降、行(水平)方向を集約する場合はpl.sum_horizontal(複数列)等を使用してください。

.over()を適用するとグループごとの計算値を返します(pandasのgroupby.transform())。

df.select(pl.col("Integer", "Float").sum().over("String"))
# shape: (4, 2)
# ┌─────────┬───────┐
# │ Integer ┆ Float │
# │ ---     ┆ ---   │
# │ i64     ┆ f64   │
# ╞═════════╪═══════╡
# │ 4       ┆ 4.0   │
# │ 6       ┆ 6.0   │
# │ 4       ┆ 4.0   │
# │ 6       ┆ 6.0   │
# └─────────┴───────┘

条件分岐:when()

条件分岐(np.where()に相当するもの)はwhen(条件文).then(Trueの場合).otherwise(Flaseの場合)という特殊なチェーンメソッドによって行います。

以前は.then()および.otherwise()に文字列の値を直接与えることが可能でしたが、他の関数の構文(文字列は通常列名を意味する)と揃えるため、バージョン0.18.9以降この書き方は非推奨となりました。.then()および.otherwise()には原則エクスプレッションを与えてください。

"""
Integer列に基づいて、2より大きい場合 "big" 、そうでない場合 "small" の新しい列を作る
"""
# バージョン0.18.8以前
df.select(
    pl.when(pl.col("Integer") > 2).then("big").otherwise("small")
)

# バージョン0.18.9以降
df.select(
    pl.when(pl.col("Integer") > 2).then(pl.lit("big")).otherwise(pl.lit("small"))
)
df.select(
    pl.when(pl.col("Integer") > 2).then(pl.lit("big")).otherwise(pl.lit("small"))
)
# shape: (4, 1)
# ┌─────────┐
# │ literal │
# │ ---     │
# │ str     │
# ╞═════════╡
# │ small   │
# │ small   │
# │ big     │
# │ big     │
# └─────────┘

df.select(
    pl.when(pl.col("Integer") > 2)
    .then(pl.col("String").str.to_uppercase())
    .otherwise(pl.col("String").str.replace_all(r".", "x"))
)
# shape: (4, 1)
# ┌────────┐
# │ String │
# │ ---    │
# │ str    │
# ╞════════╡
# │ xxxx   │
# │ xxxxx  │
# │ TEST   │
# │ TRAIN  │
# └────────┘

複数の条件によって異なる結果としたい場合(NumPyのnp.select()や、ExcelのIFS()のように)、when().then().when().then().....when().then().otherwise()とします。

df.select(
    pl.when(pl.col("Integer") == 1)
    .then(pl.lit("one"))
    .when(pl.col("Integer") == 2)
    .then(pl.lit("two"))
    .when(pl.col("Integer") == 3)
    .then(pl.lit("three"))
    .otherwise(pl.lit("other"))
)
# shape: (4, 1)
# ┌─────────┐
# │ literal │
# │ ---     │
# │ str     │
# ╞═════════╡
# │ one     │
# │ two     │
# │ three   │
# │ other   │
# └─────────┘

pl.coalesce()関数は、与えられた列を上から読み込んで、Noneの場合は次の列で上書きします。

new_column = pl.coalesce(
    pl.when(pl.col("Integer") == 1).then(pl.lit("one")).otherwise(None),
    pl.when(pl.col("Integer") == 2).then(pl.lit("two")).otherwise(None),
    pl.when(pl.col("Integer") == 3).then(pl.lit("three")).otherwise(pl.lit("other")),
)
df.select(new_column)
# shape: (4, 1)
# ┌─────────┐
# │ literal │
# │ ---     │
# │ str     │
# ╞═════════╡
# │ one     │
# │ two     │
# │ three   │
# │ other   │
# └─────────┘

なお、polarsにあるwhere()メソッドは、単なるfilter()の異名で、条件文を与えると行フィルタリングします。

df.select(pl.col("Float").where(pl.col("Integer") > 2))
# shape: (2, 1)
# ┌───────┐
# │ Float │
# │ ---   │
# │ f64   │
# ╞═══════╡
# │ 3.0   │
# │ 4.0   │
# └───────┘

複数のデータフレームの結合・マージ

pl.concat()による結合(スタック・連結)

pl.concat()関数を使って、複数のデータフレームを結合することができます。

デフォルトでは、データフレームをnp.vstack()のように縦に結合します。この操作をする場合は、結合元のデータフレーム同士は列数と列名が一致している必要があります

df1 = pl.DataFrame(
    {"A": ["A0", "A1"], "B": ["B0", "B1"], "C": ["C0", "C1"], "D": ["D0", "D1"]}
)
df2 = pl.DataFrame(
    {"A": ["A2", "A3"], "B": ["B2", "B3"], "C": ["C2", "C3"], "D": ["D2", "D3"]}
)
df3 = pl.DataFrame(
    {"A": ["A4", "A5"], "B": ["B4", "B5"], "C": ["C4", "C5"], "D": ["D4", "D5"]}
)
df1
# shape: (2, 4)
# ┌─────┬─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   ┆ D   │
# │ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ str ┆ str ┆ str │
# ╞═════╪═════╪═════╪═════╡
# │ A0  ┆ B0  ┆ C0  ┆ D0  │
# │ A1  ┆ B1  ┆ C1  ┆ D1  │
# └─────┴─────┴─────┴─────┘

pl.concat([df1, df2, df3])
# shape: (6, 4)
# ┌─────┬─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   ┆ D   │
# │ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ str ┆ str ┆ str │
# ╞═════╪═════╪═════╪═════╡
# │ A0  ┆ B0  ┆ C0  ┆ D0  │
# │ A1  ┆ B1  ┆ C1  ┆ D1  │
# │ A2  ┆ B2  ┆ C2  ┆ D2  │
# │ A3  ┆ B3  ┆ C3  ┆ D3  │
# │ A4  ┆ B4  ┆ C4  ┆ D4  │
# │ A5  ┆ B5  ┆ C5  ┆ D5  │
# └─────┴─────┴─────┴─────┘

列が一致していないデータフレームを結合したい場合はhow="diagonal"を指定します。列名を基準にした完全外部結合となり、足りない部分はnullで埋められます。

df1 = pl.DataFrame(
    {"A": ["A0", "A1"], "B": ["B0", "B1"], "C": ["C0", "C1"]}
)
df2 = pl.DataFrame(
    {"A": ["A2", "A3"], "B": ["B2", "B3"], "D": ["D2", "D3"]}
)
df3 = pl.DataFrame(
    {"A": ["A4", "A5"], "B": ["B4", "B5"], "C": ["C4", "C5"], "D": ["D4", "D5"]}
)

pl.concat([df1, df2, df3], how="diagonal")
# shape: (6, 4)
# ┌─────┬─────┬──────┬──────┐
# │ A   ┆ B   ┆ C    ┆ D    │
# │ --- ┆ --- ┆ ---  ┆ ---  │
# │ str ┆ str ┆ str  ┆ str  │
# ╞═════╪═════╪══════╪══════╡
# │ A0  ┆ B0  ┆ C0   ┆ null │
# │ A1  ┆ B1  ┆ C1   ┆ null │
# │ A2  ┆ B2  ┆ null ┆ D2   │
# │ A3  ┆ B3  ┆ null ┆ D3   │
# │ A4  ┆ B4  ┆ C4   ┆ D4   │
# │ A5  ┆ B5  ┆ C5   ┆ D5   │
# └─────┴─────┴──────┴──────┘

pl.concat()関数で、how="horizontal"を指定すると、np.hstack()のようにデータフレームを横に結合できます。この操作をする場合は、結合元のデータフレームの列名が全て異なっている必要があります。行数が異なる場合は、最も長いデータフレームに合わせてnullで埋められます。

df1 = pl.DataFrame({"A": ["A0", "A1", "A2"], "B": ["B0", "B1", "B2"]})
df2 = pl.DataFrame({"C": ["C0", "C1"], "D": ["D0", "D1"]})

pl.concat([df1, df2], how="horizontal")
# shape: (3, 4)
# ┌─────┬─────┬──────┬──────┐
# │ A   ┆ B   ┆ C    ┆ D    │
# │ --- ┆ --- ┆ ---  ┆ ---  │
# │ str ┆ str ┆ str  ┆ str  │
# ╞═════╪═════╪══════╪══════╡
# │ A0  ┆ B0  ┆ C0   ┆ D0   │
# │ A1  ┆ B1  ┆ C1   ┆ D1   │
# │ A2  ┆ B2  ┆ null ┆ null │
# └─────┴─────┴──────┴──────┘

pl.concat()は、データフレーム同士の結合専用で、データフレームとシリーズの結合はできません。データフレームとシリーズの結合はDataFrame.with_columns()などを使用してください。

DataFrame.join()による結合(マージ)

DataFrame.join()メソッドを使って、2つのデータフレームを結合することができます。使い方はpandasのDataFrame.merge()とほぼ同じです。

デフォルト(how="inner")では内部結合(2つのデータフレームに共通して存在するキーのみを保持)になります。

left = pl.DataFrame(
    {
        "key1": ["K0", "K0", "K1", "K2"],
        "key2": ["K0", "K1", "K0", "K1"],
        "A": ["A0", "A1", "A2", "A3"],
        "B": ["B0", "B1", "B2", "B3"],
    }
)
# shape: (4, 4)
# ┌──────┬──────┬─────┬─────┐
# │ key1 ┆ key2 ┆ A   ┆ B   │
# │ ---  ┆ ---  ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╡
# │ K0   ┆ K0   ┆ A0  ┆ B0  │
# │ K0   ┆ K1   ┆ A1  ┆ B1  │
# │ K1   ┆ K0   ┆ A2  ┆ B2  │
# │ K2   ┆ K1   ┆ A3  ┆ B3  │
# └──────┴──────┴─────┴─────┘

right = pl.DataFrame(
    {
        "key1": ["K0", "K1", "K1", "K2"],
        "key2": ["K0", "K0", "K0", "K0"],
        "C": ["C0", "C1", "C2", "C3"],
        "D": ["D0", "D1", "D2", "D3"],
    }
)
# shape: (4, 4)
# ┌──────┬──────┬─────┬─────┐
# │ key1 ┆ key2 ┆ C   ┆ D   │
# │ ---  ┆ ---  ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╡
# │ K0   ┆ K0   ┆ C0  ┆ D0  │
# │ K1   ┆ K0   ┆ C1  ┆ D1  │
# │ K1   ┆ K0   ┆ C2  ┆ D2  │
# │ K2   ┆ K0   ┆ C3  ┆ D3  │
# └──────┴──────┴─────┴─────┘

left.join(right, on=["key1", "key2"])
# shape: (3, 6)
# ┌──────┬──────┬─────┬─────┬─────┬─────┐
# │ key1 ┆ key2 ┆ A   ┆ B   ┆ C   ┆ D   │
# │ ---  ┆ ---  ┆ --- ┆ --- ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╪═════╪═════╡
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ C0  ┆ D0  │
# │ K1   ┆ K0   ┆ A2  ┆ B2  ┆ C1  ┆ D1  │
# │ K1   ┆ K0   ┆ A2  ┆ B2  ┆ C2  ┆ D2  │
# └──────┴──────┴─────┴─────┴─────┴─────┘

左外部結合(左フレームのキーに合わせて右フレームを結合)はhow="left"を指定します。pandasと異なり右外部結合はできません。

left.join(right, on=["key1", "key2"], how="left")
# shape: (5, 6)
# ┌──────┬──────┬─────┬─────┬──────┬──────┐
# │ key1 ┆ key2 ┆ A   ┆ B   ┆ C    ┆ D    │
# │ ---  ┆ ---  ┆ --- ┆ --- ┆ ---  ┆ ---  │
# │ str  ┆ str  ┆ str ┆ str ┆ str  ┆ str  │
# ╞══════╪══════╪═════╪═════╪══════╪══════╡
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ C0   ┆ D0   │
# │ K0   ┆ K1   ┆ A1  ┆ B1  ┆ null ┆ null │
# │ K1   ┆ K0   ┆ A2  ┆ B2  ┆ C1   ┆ D1   │
# │ K1   ┆ K0   ┆ A2  ┆ B2  ┆ C2   ┆ D2   │
# │ K2   ┆ K1   ┆ A3  ┆ B3  ┆ null ┆ null │
# └──────┴──────┴─────┴─────┴──────┴──────┘

完全外部結合はhow="outer"またはhow="outer_coalesce"を指定します。

  • how="outer"は左右フレームのキー列が維持された状態で結合されます。列名が重複する場合はsuffix=引数で右フレームのキー列名に接尾辞を付加できます(デフォルトは"_right")。
  • how="outer_coalesce"は、左右フレームのキー列を融合させた新しいキー列を作ります。
left.join(right, on=["key1", "key2"], how="outer")
# shape: (6, 8)
# ┌──────┬──────┬──────┬──────┬────────────┬────────────┬──────┬──────┐
# │ key1 ┆ key2 ┆ A    ┆ B    ┆ key1_right ┆ key2_right ┆ C    ┆ D    │
# │ ---  ┆ ---  ┆ ---  ┆ ---  ┆ ---        ┆ ---        ┆ ---  ┆ ---  │
# │ str  ┆ str  ┆ str  ┆ str  ┆ str        ┆ str        ┆ str  ┆ str  │
# ╞══════╪══════╪══════╪══════╪════════════╪════════════╪══════╪══════╡
# │ K0   ┆ K0   ┆ A0   ┆ B0   ┆ K0         ┆ K0         ┆ C0   ┆ D0   │
# │ K1   ┆ K0   ┆ A2   ┆ B2   ┆ K1         ┆ K0         ┆ C1   ┆ D1   │
# │ K1   ┆ K0   ┆ A2   ┆ B2   ┆ K1         ┆ K0         ┆ C2   ┆ D2   │
# │ null ┆ null ┆ null ┆ null ┆ K2         ┆ K0         ┆ C3   ┆ D3   │
# │ K0   ┆ K1   ┆ A1   ┆ B1   ┆ null       ┆ null       ┆ null ┆ null │
# │ K2   ┆ K1   ┆ A3   ┆ B3   ┆ null       ┆ null       ┆ null ┆ null │
# └──────┴──────┴──────┴──────┴────────────┴────────────┴──────┴──────┘

left.join(right, on=["key1", "key2"], how="outer_coalesce")
# shape: (6, 6)
# ┌──────┬──────┬──────┬──────┬──────┬──────┐
# │ key1 ┆ key2 ┆ A    ┆ B    ┆ C    ┆ D    │
# │ ---  ┆ ---  ┆ ---  ┆ ---  ┆ ---  ┆ ---  │
# │ str  ┆ str  ┆ str  ┆ str  ┆ str  ┆ str  │
# ╞══════╪══════╪══════╪══════╪══════╪══════╡
# │ K0   ┆ K0   ┆ A0   ┆ B0   ┆ C0   ┆ D0   │
# │ K1   ┆ K0   ┆ A2   ┆ B2   ┆ C1   ┆ D1   │
# │ K1   ┆ K0   ┆ A2   ┆ B2   ┆ C2   ┆ D2   │
# │ K2   ┆ K0   ┆ null ┆ null ┆ C3   ┆ D3   │
# │ K2   ┆ K1   ┆ A3   ┆ B3   ┆ null ┆ null │
# │ K0   ┆ K1   ┆ A1   ┆ B1   ┆ null ┆ null │
# └──────┴──────┴──────┴──────┴──────┴──────┘

バージョン0.19.19以前はhow="outer"how="outer_coalesce"の区別は存在せず、how="outer"の指定で「現在のhow="outer_coalesce"の結合方法」が行われていました。

交差結合(how="cross")も可能です。

left.join(right, on=["key1", "key2"], how="cross")
# shape: (16, 8)
# ┌──────┬──────┬─────┬─────┬────────────┬────────────┬─────┬─────┐
# │ key1 ┆ key2 ┆ A   ┆ B   ┆ key1_right ┆ key2_right ┆ C   ┆ D   │
# │ ---  ┆ ---  ┆ --- ┆ --- ┆ ---        ┆ ---        ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str ┆ str        ┆ str        ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╪════════════╪════════════╪═════╪═════╡
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ K0         ┆ K0         ┆ C0  ┆ D0  │
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ K1         ┆ K0         ┆ C1  ┆ D1  │
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ K1         ┆ K0         ┆ C2  ┆ D2  │
# │ K0   ┆ K0   ┆ A0  ┆ B0  ┆ K2         ┆ K0         ┆ C3  ┆ D3  │
# │ ...  ┆ ...  ┆ ... ┆ ... ┆ ...        ┆ ...        ┆ ... ┆ ... │
# │ K2   ┆ K1   ┆ A3  ┆ B3  ┆ K0         ┆ K0         ┆ C0  ┆ D0  │
# │ K2   ┆ K1   ┆ A3  ┆ B3  ┆ K1         ┆ K0         ┆ C1  ┆ D1  │
# │ K2   ┆ K1   ┆ A3  ┆ B3  ┆ K1         ┆ K0         ┆ C2  ┆ D2  │
# │ K2   ┆ K1   ┆ A3  ┆ B3  ┆ K2         ┆ K0         ┆ C3  ┆ D3  │
# └──────┴──────┴─────┴─────┴────────────┴────────────┴─────┴─────┘

pandasにはない機能として、how="semi"how="anti"を指定することができます。これは結合ではなく行フィルタリングで、how="semi"は右のデータフレームに存在するキーを持つ行だけを残し、how="anti"は右のデータフレームに存在しないキーを持つ行だけを残します。

left.join(right, on=["key1", "key2"], how="semi")
# shape: (2, 4)
# ┌──────┬──────┬─────┬─────┐
# │ key1 ┆ key2 ┆ A   ┆ B   │
# │ ---  ┆ ---  ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╡
# │ K0   ┆ K0   ┆ A0  ┆ B0  │
# │ K1   ┆ K0   ┆ A2  ┆ B2  │
# └──────┴──────┴─────┴─────┘

left.join(right, on=["key1", "key2"], how="anti")
# shape: (2, 4)
# ┌──────┬──────┬─────┬─────┐
# │ key1 ┆ key2 ┆ A   ┆ B   │
# │ ---  ┆ ---  ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str ┆ str │
# ╞══════╪══════╪═════╪═════╡
# │ K0   ┆ K1   ┆ A1  ┆ B1  │
# │ K2   ┆ K1   ┆ A3  ┆ B3  │
# └──────┴──────┴─────┴─────┘

GroupBy

グループごとに関数を適用する場合は.group_by()メソッドを使います。pandasの.groupby()と同様に、列名または列名のリストをキーとして渡すことができます。結果のデータフレームは、pandasではグループキーに基づいてソートされていますが、polarsでは行の並び順はランダムです(同じコードでも計算するたびに変わります)。

バージョン0.18.15以前は、.group_by()メソッドは.groupby()という名前でした。

rng = np.random.default_rng(0)
df = pl.DataFrame(
    {
        "A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
        "B": ["one", "one", "two", "three", "two", "two", "one", "three"],
        "C": rng.random(8),
        "D": rng.random(8),
    }
)
# shape: (8, 4)
# ┌─────┬───────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╡
# │ foo ┆ one   ┆ 0.636962 ┆ 0.543625 │
# │ bar ┆ one   ┆ 0.269787 ┆ 0.935072 │
# │ foo ┆ two   ┆ 0.040974 ┆ 0.815854 │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 │
# │ foo ┆ two   ┆ 0.81327  ┆ 0.857404 │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 │
# │ foo ┆ one   ┆ 0.606636 ┆ 0.729655 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 │
# └─────┴───────┴──────────┴──────────┘

df.group_by("A", "B").sum()
# shape: (6, 4)
# ┌─────┬───────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╡
# │ foo ┆ one   ┆ 1.243597 ┆ 1.27328  │
# │ foo ┆ two   ┆ 0.854244 ┆ 1.673258 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 │
# │ bar ┆ one   ┆ 0.269787 ┆ 0.935072 │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 │
# └─────┴───────┴──────────┴──────────┘

バージョン0.16.5以前は、複数の列をグループキーにする場合は.group_by()(当時は.groupby())に列名のリストを渡すとされていましたが、バージョン0.16.6から直接列名を複数渡すことができるようになりました。

# バージョン0.16.5以前の記法(以降のバージョンでも可能)
df.groupby(["A", "B"]).sum()

# バージョン0.16.6以降の記法
df.groupby("A", "B").sum()

グループキーにエクスプレッションを渡すこともできます。

df.group_by("A", pl.col("B").str.to_uppercase().alias("BIG_B")).first()
# shape: (6, 5)
# ┌─────┬───────┬───────┬──────────┬──────────┐
# │ A   ┆ BIG_B ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪═══════╪══════════╪══════════╡
# │ foo ┆ ONE   ┆ one   ┆ 0.636962 ┆ 0.543625 │
# │ foo ┆ TWO   ┆ two   ┆ 0.040974 ┆ 0.815854 │
# │ foo ┆ THREE ┆ three ┆ 0.729497 ┆ 0.175656 │
# │ bar ┆ ONE   ┆ one   ┆ 0.269787 ┆ 0.935072 │
# │ bar ┆ TWO   ┆ two   ┆ 0.912756 ┆ 0.033586 │
# │ bar ┆ THREE ┆ three ┆ 0.016528 ┆ 0.002739 │
# └─────┴───────┴───────┴──────────┴──────────┘

polarsではグループキーにNaNnullがある行も保持されます(pandasはデフォルトではグループキーがnanNAの行は削除されます)。

dfna = pl.DataFrame(
    [[np.nan, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]], schema="ABCD"
)
# shape: (3, 4)
# ┌─────┬──────┬─────┬─────┐
# │ A   ┆ B    ┆ C   ┆ D   │
# │ --- ┆ ---  ┆ --- ┆ --- │
# │ f64 ┆ i64  ┆ i64 ┆ i64 │
# ╞═════╪══════╪═════╪═════╡
# │ NaN ┆ 1    ┆ 2   ┆ 1   │
# │ 2.0 ┆ null ┆ 1   ┆ 2   │
# │ 3.0 ┆ 4    ┆ 3   ┆ 2   │
# └─────┴──────┴─────┴─────┘

dfna.group_by("A").sum()
# shape: (3, 4)
# ┌─────┬──────┬─────┬─────┐
# │ A   ┆ B    ┆ C   ┆ D   │
# │ --- ┆ ---  ┆ --- ┆ --- │
# │ f64 ┆ i64  ┆ i64 ┆ i64 │
# ╞═════╪══════╪═════╪═════╡
# │ NaN ┆ 1    ┆ 2   ┆ 1   │
# │ 2.0 ┆ 0    ┆ 1   ┆ 2   │
# │ 3.0 ┆ 4    ┆ 3   ┆ 2   │
# └─────┴──────┴─────┴─────┘

dfna.group_by("B").sum()
# shape: (3, 4)
# ┌──────┬─────┬─────┬─────┐
# │ B    ┆ A   ┆ C   ┆ D   │
# │ ---  ┆ --- ┆ --- ┆ --- │
# │ i64  ┆ f64 ┆ i64 ┆ i64 │
# ╞══════╪═════╪═════╪═════╡
# │ 4    ┆ 3.0 ┆ 3   ┆ 2   │
# │ null ┆ 2.0 ┆ 1   ┆ 2   │
# │ 1    ┆ NaN ┆ 2   ┆ 1   │
# └──────┴─────┴─────┴─────┘

GroupByオブジェクト

GroupByオブジェクトはイテレータとしても動作します。イテレータとして用いると、itertoolsやpandasと同様、(グループ名, データフレーム)のタプルを取得できます。

バージョン0.15までは、GroupByオブジェクトをイテレータとして用いるとデータフレームのみが返されましたが、バージョン0.16から(グループ名, データフレーム)のタプルが返される挙動(itertoolsやpandasと同様)に変更されました。

grouped = df.group_by("A")
type(grouped)
# <class 'polars.dataframe.group_by.GroupBy'>

for group_name, group_df in grouped:
    print(f"{group_name = }")
    print(group_df)
    print("---")
# group_name = 'foo'
# shape: (5, 4)
# ┌─────┬───────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╡
# │ foo ┆ one   ┆ 0.636962 ┆ 0.543625 │
# │ foo ┆ two   ┆ 0.040974 ┆ 0.815854 │
# │ foo ┆ two   ┆ 0.81327  ┆ 0.857404 │
# │ foo ┆ one   ┆ 0.606636 ┆ 0.729655 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 │
# └─────┴───────┴──────────┴──────────┘
# ---
# group_name = 'bar'
# shape: (3, 4)
# ┌─────┬───────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╡
# │ bar ┆ one   ┆ 0.269787 ┆ 0.935072 │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 │
# └─────┴───────┴──────────┴──────────┘
# ---

集計

集計は、グループキー以外のすべての列に適用する場合は、既に紹介したようにGroupbyオブジェクトのメソッドを使います。

df.group_by("A", "B").head()
# shape: (8, 4)
# ┌─────┬───────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C        ┆ D        │
# │ --- ┆ ---   ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╡
# │ foo ┆ one   ┆ 0.636962 ┆ 0.543625 │
# │ foo ┆ one   ┆ 0.606636 ┆ 0.729655 │
# │ foo ┆ two   ┆ 0.040974 ┆ 0.815854 │
# │ foo ┆ two   ┆ 0.81327  ┆ 0.857404 │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 │
# │ bar ┆ one   ┆ 0.269787 ┆ 0.935072 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 │
# └─────┴───────┴──────────┴──────────┘

df.group_by("A", "B").count()
# shape: (6, 3)
# ┌─────┬───────┬───────┐
# │ A   ┆ B     ┆ count │
# │ --- ┆ ---   ┆ ---   │
# │ str ┆ str   ┆ u32   │
# ╞═════╪═══════╪═══════╡
# │ foo ┆ one   ┆ 2     │
# │ foo ┆ three ┆ 1     │
# │ bar ┆ two   ┆ 1     │
# │ foo ┆ two   ┆ 2     │
# │ bar ┆ one   ┆ 1     │
# │ bar ┆ three ┆ 1     │
# └─────┴───────┴───────┘

列ごとに適用する場合は、.agg()にエクスプレッションを渡します。polarsでは列名の重複を許さないので、同じ列に異なる関数を適用した場合は.alias().name.prefix().name.suffix()などで適宜列名を調整する必要があります。

df.group_by("A", "B").agg(
    pl.sum("*").name.suffix("_sum"),
    pl.mean("C").name.suffix("_mean"),
    pl.std("D").name.suffix("_std"),
)
# shape: (6, 6)
# ┌─────┬───────┬──────────┬──────────┬──────────┬──────────┐
# │ A   ┆ B     ┆ C_sum    ┆ D_sum    ┆ C_mean   ┆ D_std    │
# │ --- ┆ ---   ┆ ---      ┆ ---      ┆ ---      ┆ ---      │
# │ str ┆ str   ┆ f64      ┆ f64      ┆ f64      ┆ f64      │
# ╞═════╪═══════╪══════════╪══════════╪══════════╪══════════╡
# │ bar ┆ one   ┆ 0.269787 ┆ 0.935072 ┆ 0.269787 ┆ null     │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 ┆ 0.016528 ┆ null     │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 ┆ 0.912756 ┆ null     │
# │ foo ┆ one   ┆ 1.243597 ┆ 1.27328  ┆ 0.621799 ┆ 0.131543 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 ┆ 0.729497 ┆ null     │
# │ foo ┆ two   ┆ 0.854244 ┆ 1.673258 ┆ 0.427122 ┆ 0.029381 │
# └─────┴───────┴──────────┴──────────┴──────────┴──────────┘

バージョン0.16.6以前は、.agg()エクスプレッションのリストを渡すとされていましたが、バージョン0.16.7から直接エクスプレッションを複数渡すことができるようになりました。

# バージョン0.16.6以前の記法(以降のバージョンでも可能)
df.groupby(["A", "B"]).agg(
    [
        pl.sum("*").suffix("_sum"),
        pl.mean("C").suffix("_mean"),
        pl.std("D").suffix("_std"),
    ]
)

# バージョン0.16.7以降の記法
df.groupby("A", "B").agg(
    pl.sum("*").suffix("_sum"),
    pl.mean("C").suffix("_mean"),
    pl.std("D").suffix("_std"),
)

ピボットテーブル

DataFrame.pivot()でピボットテーブルを作成します。引数はvalue=index=columns=aggregate_function=の順に渡します。pandasのDataFrame.pivot_table()に相当します。

df.pivot("C", "A", "B", "sum")
# shape: (2, 4)
# ┌─────┬──────────┬──────────┬──────────┐
# │ A   ┆ one      ┆ two      ┆ three    │
# │ --- ┆ ---      ┆ ---      ┆ ---      │
# │ str ┆ f64      ┆ f64      ┆ f64      │
# ╞═════╪══════════╪══════════╪══════════╡
# │ foo ┆ 1.243597 ┆ 0.854244 ┆ 0.729497 │
# │ bar ┆ 0.269787 ┆ 0.912756 ┆ 0.016528 │
# └─────┴──────────┴──────────┴──────────┘

なお、ピボットテーブルを戻す場合は.melt()を用います。pandasと異なりインデックス列がないので.stack()という名前のメソッドはありません。

pivot_table = df.pivot("C", "A", "B", "sum")
pivot_table.melt("A", ["one", "two", "three"])
# shape: (6, 3)
# ┌─────┬──────────┬──────────┐
# │ A   ┆ variable ┆ value    │
# │ --- ┆ ---      ┆ ---      │
# │ str ┆ str      ┆ f64      │
# ╞═════╪══════════╪══════════╡
# │ foo ┆ one      ┆ 1.243597 │
# │ bar ┆ one      ┆ 0.269787 │
# │ foo ┆ two      ┆ 0.854244 │
# │ bar ┆ two      ┆ 0.912756 │
# │ foo ┆ three    ┆ 0.729497 │
# │ bar ┆ three    ┆ 0.016528 │
# └─────┴──────────┴──────────┘

欠損値・欠損データの扱い

データフレーム作成の際に要素としてNoneが渡されると、欠損値nullとみなされます。ただし、浮動小数型にキャストできるときにnp.nanが渡さた場合には、NaNとみなされます。

全てのデータ型が要素としてNaNを持つことができ、浮動小数データ型のシリーズではnullNaNが共存できます。

df_missing = pl.DataFrame(
    {"A": [1, 2, None, 4, None, None], "B": [np.nan, 4, 3, 2, np.nan, None]}
)

df_missing
# shape: (6, 2)
# ┌──────┬──────┐
# │ A    ┆ B    │
# │ ---  ┆ ---  │
# │ i64  ┆ f64  │
# ╞══════╪══════╡
# │ 1    ┆ NaN  │
# │ 2    ┆ 4.0  │
# │ null ┆ 3.0  │
# │ 4    ┆ 2.0  │
# │ null ┆ NaN  │
# │ null ┆ null │
# └──────┴──────┘

欠損値の確認

nullかどうかの真偽値は.is_null()メソッドを、NaNかどうかの真偽値は.is_nan()メソッドで得られます。これらはどちらもシリーズまたはエクスプレッションのみに存在するメソッドで、データフレームのメソッドではありません。

df_missing.with_columns(
    A_is_null=pl.col("A").is_null(),
    B_is_null=pl.col("B").is_null(),
    B_is_nan=pl.col("B").is_nan(),
)
# shape: (6, 5)
# ┌──────┬──────┬───────────┬───────────┬──────────┐
# │ A    ┆ B    ┆ A_is_null ┆ B_is_null ┆ B_is_nan │
# │ ---  ┆ ---  ┆ ---       ┆ ---       ┆ ---      │
# │ i64  ┆ f64  ┆ bool      ┆ bool      ┆ bool     │
# ╞══════╪══════╪═══════════╪═══════════╪══════════╡
# │ 1    ┆ NaN  ┆ false     ┆ false     ┆ true     │
# │ 2    ┆ 4.0  ┆ false     ┆ false     ┆ false    │
# │ null ┆ 3.0  ┆ true      ┆ false     ┆ false    │
# │ 4    ┆ 2.0  ┆ false     ┆ false     ┆ false    │
# │ null ┆ NaN  ┆ true      ┆ false     ┆ true     │
# │ null ┆ null ┆ true      ┆ true      ┆ null     │
# └──────┴──────┴───────────┴───────────┴──────────┘

.null_count()メソッドはnullの数を集計します。

df_missing.null_count()
# shape: (1, 2)
# ┌─────┬─────┐
# │ A   ┆ B   │
# │ --- ┆ --- │
# │ u32 ┆ u32 │
# ╞═════╪═════╡
# │ 3   ┆ 1   │
# └─────┴─────┘

シリーズまたはエクスプレッションの.count()メソッドはnullを除いたデータの数を集計します。

df_missing.select(
    pl.all().count().name.suffix("_count"),
    pl.all().len().name.suffix("_len"),
)
# shape: (1, 4)
# ┌─────────┬─────────┬───────┬───────┐
# │ A_count ┆ B_count ┆ A_len ┆ B_len │
# │ ---     ┆ ---     ┆ ---   ┆ ---   │
# │ u32     ┆ u32     ┆ u32   ┆ u32   │
# ╞═════════╪═════════╪═══════╪═══════╡
# │ 3       ┆ 5       ┆ 6     ┆ 6     │
# └─────────┴─────────┴───────┴───────┘

欠損値の伝搬

nullまたはNaNに対する数値演算ではnullまたはNaNが維持されます。nullNaNの組み合わせで演算を実行した場合はnullが優先されます。

df_missing.with_columns(A_plus_B=pl.col("A") + pl.col("B"))
# shape: (6, 3)
# ┌──────┬──────┬──────────┐
# │ A    ┆ B    ┆ A_plus_B │
# │ ---  ┆ ---  ┆ ---      │
# │ i64  ┆ f64  ┆ f64      │
# ╞══════╪══════╪══════════╡
# │ 1    ┆ NaN  ┆ NaN      │
# │ 2    ┆ 4.0  ┆ 6.0      │
# │ null ┆ 3.0  ┆ null     │
# │ 4    ┆ 2.0  ┆ 6.0      │
# │ null ┆ NaN  ┆ null     │
# │ null ┆ null ┆ null     │
# └──────┴──────┴──────────┘

nullに対する比較演算の結果は常にnullになります。nullNaNとの比較演算の結果もnullになります。NaNと数値との比較演算の結果は常にFalseになります。

df_missing.with_columns(
    gt=(pl.col("A") > pl.col("B")),
    eq=(pl.col("A") == pl.col("B")),
)
# shape: (6, 4)
# ┌──────┬──────┬───────┬───────┐
# │ A    ┆ B    ┆ gt    ┆ eq    │
# │ ---  ┆ ---  ┆ ---   ┆ ---   │
# │ i64  ┆ f64  ┆ bool  ┆ bool  │
# ╞══════╪══════╪═══════╪═══════╡
# │ 1    ┆ NaN  ┆ false ┆ false │
# │ 2    ┆ 4.0  ┆ false ┆ false │
# │ null ┆ 3.0  ┆ null  ┆ null  │
# │ 4    ┆ 2.0  ┆ true  ┆ false │
# │ null ┆ NaN  ┆ null  ┆ null  │
# │ null ┆ null ┆ null  ┆ null  │
# └──────┴──────┴───────┴───────┘

バージョン0.19.19以前はNaN == NaNの結果はFalseでしたが(ほとんどの計算ライブラリと同様)、バージョン0.20.0以降、NaN == NaNの結果はTrueとなります。

集計関数はnullを無視して計算しますが、NaNを含む場合はNaNになります。

df_missing.sum()
# shape: (1, 2)
# ┌─────┬─────┐
# │ A   ┆ B   │
# │ --- ┆ --- │
# │ i64 ┆ f64 │
# ╞═════╪═════╡
# │ 7   ┆ NaN │
# └─────┴─────┘

df_missing.describe()
# shape: (9, 3)
# ┌────────────┬──────────┬─────┐
# │ describe   ┆ A        ┆ B   │
# │ ---        ┆ ---      ┆ --- │
# │ str        ┆ f64      ┆ f64 │
# ╞════════════╪══════════╪═════╡
# │ count      ┆ 3.0      ┆ 5.0 │
# │ null_count ┆ 3.0      ┆ 1.0 │
# │ mean       ┆ 2.333333 ┆ NaN │
# │ std        ┆ 1.527525 ┆ NaN │
# │ min        ┆ 1.0      ┆ 2.0 │
# │ 25%        ┆ 2.0      ┆ 3.0 │
# │ 50%        ┆ 2.0      ┆ 4.0 │
# │ 75%        ┆ 4.0      ┆ NaN │
# │ max        ┆ 4.0      ┆ 4.0 │
# └────────────┴──────────┴─────┘

欠損値の穴埋め

nullを他の値に置換する場合は.fill_null()メソッドを、NaNを他の値に置換する場合は.fill_nan()メソッドを使います。

df_missing.fill_null(99)
# shape: (6, 2)
# ┌─────┬──────┐
# │ A   ┆ B    │
# │ --- ┆ ---  │
# │ i64 ┆ f64  │
# ╞═════╪══════╡
# │ 1   ┆ NaN  │
# │ 2   ┆ 4.0  │
# │ 99  ┆ 3.0  │
# │ 4   ┆ 2.0  │
# │ 99  ┆ NaN  │
# │ 99  ┆ 99.0 │
# └─────┴──────┘

df_missing.fill_nan(99)
# shape: (6, 2)
# ┌──────┬──────┐
# │ A    ┆ B    │
# │ ---  ┆ ---  │
# │ i64  ┆ f64  │
# ╞══════╪══════╡
# │ 1    ┆ 99.0 │
# │ 2    ┆ 4.0  │
# │ null ┆ 3.0  │
# │ 4    ┆ 2.0  │
# │ null ┆ 99.0 │
# │ null ┆ null │
# └──────┴──────┘

.fill_null().fill_nan()にはエクスプレッションやシリーズを渡すことができます。

df_missing.fill_null(pl.Series([11, 12, 13, 14, 15, 16]))
# shape: (6, 2)
# ┌─────┬──────┐
# │ A   ┆ B    │
# │ --- ┆ ---  │
# │ i64 ┆ f64  │
# ╞═════╪══════╡
# │ 1   ┆ NaN  │
# │ 2   ┆ 4.0  │
# │ 13  ┆ 3.0  │
# │ 4   ┆ 2.0  │
# │ 15  ┆ NaN  │
# │ 16  ┆ null │
# └─────┴──────┘

df_missing.fill_nan(pl.col("A"))
# shape: (6, 2)
# ┌──────┬──────┐
# │ A    ┆ B    │
# │ ---  ┆ ---  │
# │ i64  ┆ f64  │
# ╞══════╪══════╡
# │ 1    ┆ 1.0  │
# │ 2    ┆ 4.0  │
# │ null ┆ 3.0  │
# │ 4    ┆ 2.0  │
# │ null ┆ null │
# │ null ┆ null │
# └──────┴──────┘

.fill_null()メソッドは特定のキーワードを与えることもできます。平均値で穴埋めする場合は"mean"を渡します。

df_missing.fill_null("mean")
# shape: (6, 2)
# ┌──────┬──────┐
# │ A    ┆ B    │
# │ ---  ┆ ---  │
# │ i64  ┆ f64  │
# ╞══════╪══════╡
# │ 1    ┆ NaN  │
# │ 2    ┆ 4.0  │
# │ null ┆ 3.0  │
# │ 4    ┆ 2.0  │
# │ null ┆ NaN  │
# │ null ┆ null │
# └──────┴──────┘

このほか、"backward""forward""min""max""zero""one"を受け入れます。

テキストデータを扱う

シリーズおよびエクスプレッションには.strアクセサを介した文字列メソッドが用意されています。

df_str = pl.DataFrame(
    {"orig": ["A", "B", "Aaba", "Baca", None, "CABA", "dog", "cat"]}
)

df_str.with_columns(
    # シリーズの操作
    df_str.get_column("orig").str.len_bytes().alias("len"),
    df_str.get_column("orig").str.to_lowercase().alias("lower"),
    # エクスプレッションの操作
    pl.col("orig").str.to_uppercase().alias("upper"),
    pl.col("orig").str.slice(1, 3).alias("slice"),
)
# shape: (8, 5)
# ┌──────┬──────┬───────┬───────┬───────┐
# │ orig ┆ len  ┆ lower ┆ upper ┆ slice │
# │ ---  ┆ ---  ┆ ---   ┆ ---   ┆ ---   │
# │ str  ┆ u32  ┆ str   ┆ str   ┆ str   │
# ╞══════╪══════╪═══════╪═══════╪═══════╡
# │ A    ┆ 1    ┆ a     ┆ A     ┆       │
# │ B    ┆ 1    ┆ b     ┆ B     ┆       │
# │ Aaba ┆ 4    ┆ aaba  ┆ AABA  ┆ aba   │
# │ Baca ┆ 4    ┆ baca  ┆ BACA  ┆ aca   │
# │ null ┆ null ┆ null  ┆ null  ┆ null  │
# │ CABA ┆ 4    ┆ caba  ┆ CABA  ┆ ABA   │
# │ dog  ┆ 3    ┆ dog   ┆ DOG   ┆ og    │
# │ cat  ┆ 3    ┆ cat   ┆ CAT   ┆ at    │
# └──────┴──────┴───────┴───────┴───────┘

加算演算子+で文字列を結合できます。

df_str.select(pl.col("orig") + "_" + pl.col("orig"))
# shape: (8, 1)
# ┌───────────┐
# │ orig      │
# │ ---       │
# │ str       │
# ╞═══════════╡
# │ A_A       │
# │ B_B       │
# │ Aaba_Aaba │
# │ Baca_Baca │
# │ null      │
# │ CABA_CABA │
# │ dog_dog   │
# │ cat_cat   │
# └───────────┘

テキストの文字数を算出

文字数を数えるには.str.len_bytes()または.str.len_chars()を使います。一般的な英数字のみを扱っている場合は.str.len_bytes()を用いるのが良いですが、これは実際にはバイト数を数えているため、結合文字や全角文字を使っている場合は.str.len_chars()を使う必要があります。計算速度は.str.len_bytes()の方が高速です。

pl.DataFrame({"a": ["Tokyo", "東京", "Cafe", "Café", "Café", None]}).with_columns(
    pl.col("a").str.len_bytes().alias("len_bytes"),
    pl.col("a").str.len_chars().alias("len_chars"),
)
# shape: (6, 3)
# ┌───────┬───────────┬───────────┐
# │ a     ┆ len_bytes ┆ len_chars │
# │ ---   ┆ ---       ┆ ---       │
# │ str   ┆ u32       ┆ u32       │
# ╞═══════╪═══════════╪═══════════╡
# │ Tokyo ┆ 5         ┆ 5         │
# │ 東京  ┆ 6         ┆ 2         │
# │ Cafe  ┆ 4         ┆ 4         │
# │ Café  ┆ 5         ┆ 4         │
# │ Café  ┆ 6         ┆ 5         │
# │ null  ┆ null      ┆ null      │
# └───────┴───────────┴───────────┘

.str.len_bytes().str.len_chars()は、バージョン0.19.7以前はそれぞれ.str.lengths().str.n_chars()という名前でした。

正規表現検索で一致部分を抽出

.str.extract()メソッドは、キャプチャグループ"()"を含む正規表現を渡すと、マッチした部分を返します。

デフォルトでは最初のマッチグループ(つまり"$1")を返します。"$2"が欲しい場合は、group_index=キーワードに2を指定します。グループを複数取得することはできません

pl.Series(["a1a2", "b1", "c1"]).str.extract(r"([ab])?(\d)")
# shape: (3,)
# Series: '' [str]
# [
#     "a"
#     "b"
#     null
# ]

pl.Series(["a1a2", "b1", "c1"]).str.extract(r"([ab])?(\d)", group_index=2)
# shape: (3,)
# Series: '' [str]
# [
#     "1"
#     "1"
#     "1"
# ]

特定の文字列を含むかどうかを検索

.str.contains()メソッドは、渡された文字列を 含む かどうかの真偽値を返します。正規表現が使用できます。

polarsの文字列メソッドにはいわゆるmatchfullmatchはありませんので、この.str.contains()メソッドで代用してください。

pl.Series(["1", "2", "3a", "3b", "03c", "4dx"]).str.contains(r"[0-9][a-z]")
# shape: (6,)
# Series: '' [bool]
# [
#     false
#     false
#     true
#     true
#     true
#     true
# ]

文字列の先頭や末尾における一致を確認する場合は.str.starts_with().str.end_with()を使用することも可能です。

正規表現による文字列の置換

文字列の置換には.str.replace()メソッドを使います。

df_str["orig"].str.replace(r"^.a|dog", "XX-XX ")
# shape: (8,)
# Series: '' [str]
# [
#     "A"
#     "B"
#     "XX-XX ba"
#     "XX-XX ca"
#     null
#     "CABA"
#     "XX-XX "
#     "XX-XX t"
# ]

時系列データを扱う

polarsの時系列データは、Pythonのdatetimeの配列です(一方pandasやnumpyは独自のオブジェクトを使用します)。

なお、strptime()strftime()の際のフォーマット文字列("%Y/%m/%d %H:%M:%S"など)についてはこちらを参照してください。

また、いくつかの関数で時間間隔を指定する際の文字列表記は以下の通りです。

  • "y":年、"mo":月、"w":週、"d":日
  • "h":時間、"m":分、"s":秒、"ms":ミリ秒、"us":マイクロ秒、"ns":ナノ秒
  • "i":インデックス

pl.date_range(), pl.datetime_range()関数

pl.date_range()関数pl.datetime_range()関数は、pandasのpd.date_range()関数に対応します。

日時の始点・終点はPythonのdatetime.datetimeクラスで、日時間隔は文字列キーワードまたはdatetime.timedeltaで指定する必要があります。また、eager=False(デフォルト)でエクスプレッション、eager=Trueでシリーズを返します。

pl.date_range(
    datetime.datetime(2022, 1, 1),
    datetime.datetime(2022, 1, 10),
    "1d",
    eager=True,
)
# shape: (10,)
# Series: 'date' [datetime[μs]]
# [
#     2022-01-01 00:00:00
#     2022-01-02 00:00:00
#     2022-01-03 00:00:00
#     2022-01-04 00:00:00
#     2022-01-05 00:00:00
#     2022-01-06 00:00:00
#     2022-01-07 00:00:00
#     2022-01-08 00:00:00
#     2022-01-09 00:00:00
#     2022-01-10 00:00:00
# ]

pl.datetime_range(
    datetime.datetime(2022, 1, 1),
    datetime.datetime(2022, 1, 10),
    datetime.timedelta(days=1, hours=12),
    eager=True,
)
# shape: (7,)
# Series: 'datetime' [datetime[μs]]
# [
#     2022-01-01 00:00:00
#     2022-01-02 12:00:00
#     2022-01-04 00:00:00
#     2022-01-05 12:00:00
#     2022-01-07 00:00:00
#     2022-01-08 12:00:00
#     2022-01-10 00:00:00
# ]

pl.date_range()関数とpl.datetime_range()関数は、バージョン0.20.0時点ではほぼ同じ挙動を示します。将来的には、結果として欲しいデータ型(それぞれpl.Dateまたはpl.Datetime)によって使い分けることとされています。

文字列から時系列に変換する.str.strptime()

polarsでは、シリーズ・エクスプレッションの文字列メソッドにstrptime()が実装されています。第一引数にデータ型、第二引数にフォーマット文字列を渡します。

df_drange = pl.DataFrame(
    {
        "drange_str": [
            "2022-01-01 00:00:00",
            "2022-01-02 12:00:00",
            "2022-01-04 00:00:00",
            "2022-01-05 12:00:00",
            "2022-01-07 00:00:00",
            "2022-01-08 12:00:00",
            "2022-01-10 00:00:00",
        ]
    }
)

df_drange.get_column("drange_str").dtype
# Utf8

df_drange.get_column("drange_str").str.strptime(pl.Datetime).dtype
# Datetime(time_unit='us', time_zone=None)

new_column = pl.col("drange_str").str.strptime(pl.Datetime).alias("drange")
df_drange.with_columns(new_column)
# shape: (7, 2)
# ┌─────────────────────┬─────────────────────┐
# │ drange_str          ┆ drange              │
# │ ---                 ┆ ---                 │
# │ str                 ┆ datetime[μs]        │
# ╞═════════════════════╪═════════════════════╡
# │ 2022-01-01 00:00:00 ┆ 2022-01-01 00:00:00 │
# │ 2022-01-02 12:00:00 ┆ 2022-01-02 12:00:00 │
# │ 2022-01-04 00:00:00 ┆ 2022-01-04 00:00:00 │
# │ 2022-01-05 12:00:00 ┆ 2022-01-05 12:00:00 │
# │ 2022-01-07 00:00:00 ┆ 2022-01-07 00:00:00 │
# │ 2022-01-08 12:00:00 ┆ 2022-01-08 12:00:00 │
# │ 2022-01-10 00:00:00 ┆ 2022-01-10 00:00:00 │
# └─────────────────────┴─────────────────────┘

なお、.str.to_date()/.str.to_datetime()メソッドでも同様の変換が可能です。

時系列データから他のデータへの変換

年月日・時分秒・エポックタイムへの変換は.dtを介して各メソッドから行います(pandasと違い属性アクセスではありません)。

df_drange = df_drange.select(new_column)
df_drange.select(
    pl.col("drange"),
    pl.col("drange").dt.year().alias("year"),
    pl.col("drange").dt.month().alias("month"),
    pl.col("drange").dt.day().alias("day"),
    pl.col("drange").dt.hour().alias("hour"),
    pl.col("drange").dt.minute().alias("minute"),
    pl.col("drange").dt.second().alias("second"),
    pl.col("drange").dt.epoch().alias("epoch"),
)
# shape: (7, 8)
# ┌─────────────────────┬──────┬───────┬─────┬──────┬────────┬────────┬──────────────────┐
# │ drange              ┆ year ┆ month ┆ day ┆ hour ┆ minute ┆ second ┆ epoch            │
# │ ---                 ┆ ---  ┆ ---   ┆ --- ┆ ---  ┆ ---    ┆ ---    ┆ ---              │
# │ datetime[μs]        ┆ i32  ┆ u32   ┆ u32 ┆ u32  ┆ u32    ┆ u32    ┆ i64              │
# ╞═════════════════════╪══════╪═══════╪═════╪══════╪════════╪════════╪══════════════════╡
# │ 2022-01-01 00:00:00 ┆ 2022 ┆ 1     ┆ 1   ┆ 0    ┆ 0      ┆ 0      ┆ 1640995200000000 │
# │ 2022-01-02 12:00:00 ┆ 2022 ┆ 1     ┆ 2   ┆ 12   ┆ 0      ┆ 0      ┆ 1641124800000000 │
# │ 2022-01-04 00:00:00 ┆ 2022 ┆ 1     ┆ 4   ┆ 0    ┆ 0      ┆ 0      ┆ 1641254400000000 │
# │ 2022-01-05 12:00:00 ┆ 2022 ┆ 1     ┆ 5   ┆ 12   ┆ 0      ┆ 0      ┆ 1641384000000000 │
# │ 2022-01-07 00:00:00 ┆ 2022 ┆ 1     ┆ 7   ┆ 0    ┆ 0      ┆ 0      ┆ 1641513600000000 │
# │ 2022-01-08 12:00:00 ┆ 2022 ┆ 1     ┆ 8   ┆ 12   ┆ 0      ┆ 0      ┆ 1641643200000000 │
# │ 2022-01-10 00:00:00 ┆ 2022 ┆ 1     ┆ 10  ┆ 0    ┆ 0      ┆ 0      ┆ 1641772800000000 │
# └─────────────────────┴──────┴───────┴─────┴──────┴────────┴────────┴──────────────────┘

文字列への変換は.dt.strftime()にフォーマット文字列を渡します。

df_drange.select(
    pl.col("drange").dt.strftime("%Y/%m/%d %H:%M:%S").alias("str1"),
    pl.col("drange").dt.strftime("%d-%m-%Y %H:%M").alias("str2"),
)
# shape: (7, 2)
# ┌─────────────────────┬──────────────────┐
# │ str1                ┆ str2             │
# │ ---                 ┆ ---              │
# │ str                 ┆ str              │
# ╞═════════════════════╪══════════════════╡
# │ 2022/01/01 00:00:00 ┆ 01-01-2022 00:00 │
# │ 2022/01/02 12:00:00 ┆ 02-01-2022 12:00 │
# │ 2022/01/04 00:00:00 ┆ 04-01-2022 00:00 │
# │ 2022/01/05 12:00:00 ┆ 05-01-2022 12:00 │
# │ 2022/01/07 00:00:00 ┆ 07-01-2022 00:00 │
# │ 2022/01/08 12:00:00 ┆ 08-01-2022 12:00 │
# │ 2022/01/10 00:00:00 ┆ 10-01-2022 00:00 │
# └─────────────────────┴──────────────────┘

リサンプリング

polarsでは.group_by_dynamic()で時系列に基づいたグループ集計を行います。pandasの.resample()に相当します。

バージョン0.18.15以前は、.group_by_dynamic()メソッドは.groupby_dynamic()という名前でした。

url = "https://raw.githubusercontent.com/nkmk/python-snippets/77f7a51e776077396fd8f8f8c1801d54e9854479/notebook/data/src/sample_date.csv"
df = pl.read_csv(url, dtypes={"date": pl.Date}).sort("date")
# shape: (12, 3)
# ┌────────────┬───────┬───────┐
# │ date       ┆ val_1 ┆ val_2 │
# │ ---        ┆ ---   ┆ ---   │
# │ date       ┆ i64   ┆ i64   │
# ╞════════════╪═══════╪═══════╡
# │ 2017-11-01 ┆ 65    ┆ 76    │
# │ 2017-11-07 ┆ 26    ┆ 66    │
# │ 2017-11-18 ┆ 47    ┆ 47    │
# │ 2017-11-27 ┆ 20    ┆ 38    │
# │ ...        ┆ ...   ┆ ...   │
# │ 2018-01-03 ┆ 98    ┆ 76    │
# │ 2018-01-08 ┆ 48    ┆ 64    │
# │ 2018-01-19 ┆ 18    ┆ 48    │
# │ 2018-01-23 ┆ 86    ┆ 70    │
# └────────────┴───────┴───────┘

.group_by_dynamic()の第一引数にキー列(ソートされている必要があります)、every=引数に時間間隔を指定します。

df.group_by_dynamic("date", every="1y").agg(pl.col("val_1", "val_2").sum())
# shape: (2, 3)
# ┌────────────┬───────┬───────┐
# │ date       ┆ val_1 ┆ val_2 │
# │ ---        ┆ ---   ┆ ---   │
# │ date       ┆ i64   ┆ i64   │
# ╞════════════╪═══════╪═══════╡
# │ 2017-01-01 ┆ 279   ┆ 403   │
# │ 2018-01-01 ┆ 250   ┆ 258   │
# └────────────┴───────┴───────┘

df.group_by_dynamic("date", every="1mo").agg(pl.col("val_1", "val_2").sum())
# shape: (3, 3)
# ┌────────────┬───────┬───────┐
# │ date       ┆ val_1 ┆ val_2 │
# │ ---        ┆ ---   ┆ ---   │
# │ date       ┆ i64   ┆ i64   │
# ╞════════════╪═══════╪═══════╡
# │ 2017-11-01 ┆ 158   ┆ 227   │
# │ 2017-12-01 ┆ 121   ┆ 176   │
# │ 2018-01-01 ┆ 250   ┆ 258   │
# └────────────┴───────┴───────┘

every=引数は時間間隔ですが、period=引数は集計対象の時間枠の長さです。every="1mo", period="15d"は、15日間の値を月毎に合計するという意味になります。

"""
month  11                        12                        01
day    01 02 03 ... 15 16 ... 30 01 02 03 ... 15 16 ... 31 01
every  -----------1mo----------> -----------1mo---------->
period |<====15d====>|           |<====15d====>|
"""
df.group_by_dynamic("date", every="1mo", period="15d").agg(
    pl.col("val_1", "val_2").sum()
)
# shape: (3, 3)
# ┌────────────┬───────┬───────┐
# │ date       ┆ val_1 ┆ val_2 │
# │ ---        ┆ ---   ┆ ---   │
# │ date       ┆ i64   ┆ i64   │
# ╞════════════╪═══════╪═══════╡
# │ 2017-11-01 ┆ 91    ┆ 142   │
# │ 2017-12-01 ┆ 69    ┆ 114   │
# │ 2018-01-01 ┆ 146   ┆ 140   │
# └────────────┴───────┴───────┘
368
367
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
368
367