LoginSignup
292

posted at

updated at

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

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

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

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

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

基礎

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.List - リスト
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()関数は第一引数としてファイルパスを受け取り、データフレームを返します。

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()コンストラクタを使ってデータフレームを作成できます。このときcolumns引数を渡すと列名を設定できます。

rng = np.random.default_rng(0)
df = pl.DataFrame(rng.random((8, 4)), columns="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が強制されます)。

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

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()メソッドを使用することができます。表示される情報は、データ数・欠損値の数・平均値・標準偏差・最小値・最大値・中央値です。

df.describe()
# shape: (7, 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.000000 ┆ test   │
# │ max        ┆ 4.0      ┆ 4.0      ┆ 2022-04-01 00:00:00.000000 ┆ train  │
# │ median     ┆ 2.5      ┆ 2.5      ┆ null                       ┆ null   │
# └────────────┴──────────┴──────────┴────────────────────────────┴────────┘

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

[]を使った行の選択

行選択は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)), columns=list("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()メソッドには、データフレームの行数と同じ長さの真偽値配列を渡すこともできます。

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  │
# └─────────┴───────┴─────────────────────┴────────┘

なおpandasと違い、polarsの.is_in()メソッドにはセットやndarrayを渡すことはできないようです。

ユニーク行・重複行

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

  • subset=引数:識別する列を指定します。デフォルトでは、全ての列の値が一致している行のみが削除されます。
  • keep=引数:デフォルトの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 │
# └───────┴─────┴──────────┘

重複するかどうかの真偽値は.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()メソッドに列名を渡します。

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

.get_column()メソッドには列名のリストを渡すこともできますが、多分正規の使い方ではないでしょう(.select()を使用してください)。

.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
# ]

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

df.find_idx_by_name("Float")
# 1

.select()を使った列選択

データフレーム内の(1つ以上の)特定の列を選択する場合は、.select()メソッドを用います。記法の互換性のため、pandasのdf["column"]のような操作も実装されていますが、推奨されていません。

.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()に似ています)。

バージョン0.15以前は、一列追加は.with_column()、複数列の追加は.with_columns()を使うこととされていましたが、バージョン0.16で.with_columns()に統一されました。後方互換性のために.with_column()は残されていますが、バージョン0.17で廃止される予定です。

ここで、既存の列に特定の操作を行うことで新しい列をつくる場合は、シリーズ操作ではなくエクスプレッションを使うことを推奨します。例えば、以下の二つの操作は結果は同じですが、後者のエクスプレッションを用いた操作の方がメモリ・計算量の観点からは効率が良いです(なお、以下の例で用いている.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」を追加します。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)), columns=list("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 │
# └─────┴─────┴─────┘

なお、行方向の集約は.fold()という特殊な関数を使います。

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

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

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

df_num["A"].value_counts()
# shape: (5, 2)
# ┌─────┬────────┐
# │ A   ┆ counts │
# │ --- ┆ ---    │
# │ i64 ┆ u32    │
# ╞═════╪════════╡
# │ 2   ┆ 1      │
# │ 8   ┆ 2      │
# │ 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  │
# └───────┴─────────────────────┴────────┘

列名の変更

列名を任意の文字列に変更する場合は.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   │
# └─────┴─────┘

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

df.select(
    pl.col(["Integer", "Float"]).prefix("Col_"),
    pl.col("String").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      │
# └─────────────┴───────────┴────────────┘

ソート・並び替え

ソートは.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    │
# └──────┘

.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を指定します。

descending=引数は、バージョン0.16.7以前はreverse=引数という名前でした。

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      │
# └─────────┘

ただし、pl.col(複数列).sum()と、pl.sum([複数列])集計方向が異なるので注意してください

df.select(pl.col("Integer", "Float").sum())
# 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 │
# └─────┘

.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 │
# └───────┴──────────┴─────┴─────┴─────┴────────┴─────────┴──────────┴──────────┘

.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のときの値)という特殊なチェーンメソッドによって行います。

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

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

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

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

# バージョン0.16.6以前の記法(以降のバージョンでも可能)
pl.coalesce(
    [
        pl.when(pl.col("Integer") == 1).then("one").otherwise(None),
        pl.when(pl.col("Integer") == 2).then("two").otherwise(None),
        pl.when(pl.col("Integer") == 3).then("three").otherwise("other"),
    ]
)

# バージョン0.16.7以降の記法
pl.coalesce(
    pl.when(pl.col("Integer") == 1).then("one").otherwise(None),
    pl.when(pl.col("Integer") == 2).then("two").otherwise(None),
    pl.when(pl.col("Integer") == 3).then("three").otherwise("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")と完全外部結合(how="outer")も可能です。右外部結合はできません。

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 │
# └──────┴──────┴─────┴─────┴──────┴──────┘

left.join(right, on=["key1", "key2"], how="outer")
# 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 │
# └──────┴──────┴──────┴──────┴──────┴──────┘

交差結合(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にはない(ただしバージョン2.1以降で一部実装予定)ものとして、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

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

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.groupby(["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以前は、複数の列をグループキーにする場合は.groupby()列名のリストを渡すとされていましたが、バージョン0.16.6から直接列名を複数渡すことができるようになりました。

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

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

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

df.groupby("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]], columns="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.groupby("A").sum()
# 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.groupby("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.groupby("A")
type(grouped)
# polars.internals.dataframe.groupby.GroupBy

for group_name, group_df in grouped:
    print(f"{group_name = }")
    print(group_df)
    print("---")
# 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 │
# └─────┴───────┴──────────┴──────────┘
# ---
# 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 │
# └─────┴───────┴──────────┴──────────┘
# ---

集計

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

df.groupby("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.groupby("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().prefix().suffix()などで適宜列名を調整する必要があります。

df.groupby("A", "B").agg(
    pl.sum("*").suffix("_sum"),
    pl.mean("C").suffix("_mean"),
    pl.std("D").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 ┆ 0.0      │
# │ bar ┆ three ┆ 0.016528 ┆ 0.002739 ┆ 0.016528 ┆ 0.0      │
# │ bar ┆ two   ┆ 0.912756 ┆ 0.033586 ┆ 0.912756 ┆ 0.0      │
# │ foo ┆ one   ┆ 1.243597 ┆ 1.27328  ┆ 0.621799 ┆ 0.131543 │
# │ foo ┆ three ┆ 0.729497 ┆ 0.175656 ┆ 0.729497 ┆ 0.0      │
# │ 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 │
# └─────┴──────────┴──────────┴──────────┘

なお、ピボットテーブルを戻す場合、(pandasでいう).stack()は存在しませんが、.melt()が存在します。

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

df_missing.select(pl.col("A") + pl.col("B"))
# shape: (6, 1)
# ┌──────┐
# │ A    │
# │ ---  │
# │ f64  │
# ╞══════╡
# │ NaN  │
# │ 6.0  │
# │ null │
# │ 6.0  │
# │ null │
# │ null │
# └──────┘

null == nullTrueになることを除いて、nullに対する比較演算の結果は常にnullになりますが、NaNに対する比較演算の結果は(nullとの比較を除き)常にFalseになります(NaN == NaNFalseです)。

df_missing.select((pl.col("A") > pl.col("B")) | (pl.col("A") == pl.col("B")))
# shape: (6, 1)
# ┌───────┐
# │ A     │
# │ ---   │
# │ bool  │
# ╞═══════╡
# │ false │
# │ false │
# │ null  │
# │ true  │
# │ null  │
# │ true  │
# └───────┘

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

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

df_missing.describe()
# shape: (7, 3)
# ┌────────────┬──────────┬─────┐
# │ describe   ┆ A        ┆ B   │
# │ ---        ┆ ---      ┆ --- │
# │ str        ┆ f64      ┆ f64 │
# ╞════════════╪══════════╪═════╡
# │ count      ┆ 6.0      ┆ 6.0 │
# │ null_count ┆ 3.0      ┆ 1.0 │
# │ mean       ┆ 2.333333 ┆ NaN │
# │ std        ┆ 1.527525 ┆ NaN │
# │ min        ┆ 1.0      ┆ 2.0 │
# │ max        ┆ 4.0      ┆ 4.0 │
# │ median     ┆ 2.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.lengths().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.lengths()または.str.n_chars()を使います。一般的な英数字のみを扱っている場合は.str.lengths()を用いても良いですが、これは実際にはバイト数を数えているため、結合文字や全角文字を使っている場合は.str.n_chars()を使う必要があります。計算速度は.str.lengths()の方が高速です。

# shape: (4, 3)
# ┌───────┬─────────┬─────────┐
# │ orig  ┆ lengths ┆ n_chars │
# │ ---   ┆ ---     ┆ ---     │
# │ str   ┆ u32     ┆ u32     │
# ╞═══════╪═════════╪═════════╡
# │ Tokyo ┆ 5       ┆ 5       │
# │ 東京  ┆ 6       ┆ 2       │
# │ Cafe  ┆ 4       ┆ 4       │
# │ Café  ┆ 5       ┆ 4       │
# └───────┴─────────┴─────────┘

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

.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()メソッドで代用してください。なお.str.starts_with().str.end_with()も実装されています。

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.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()関数

pandasのpd.date_range()関数と同様のものがpolarsにも存在します。

polarsのpl.date_range()関数では、Pythonのdatetime.datetimeクラスで始点と終点、そして文字列キーワードかdatetime.timedeltaで間隔を指定する必要があります。

pl.date_range(
    datetime.datetime(2022, 1, 1),
    datetime.datetime(2022, 1, 10),
    "1d",
    name="drange"
)
# shape: (10,)
# Series: 'drange' [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.date_range(
    datetime.datetime(2022, 1, 1),
    datetime.datetime(2022, 1, 10),
    datetime.timedelta(days=1, hours=12),
    name="drange"
)
# shape: (7,)
# Series: 'drange' [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
# ]

文字列から時系列に変換する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(tu='us', tz=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 │
# └─────────────────────┴─────────────────────┘

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

年月日・時分秒・エポックタイムへの変換は.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では.groupby_dynamic()で時系列に基づいたグループ集計を行います。pandasの.resample()に相当します。

url = "https://raw.githubusercontent.com/nkmk/python-snippets/77f7a51e776077396fd8f8f8c1801d54e9854479/notebook/data/src/sample_date.csv"
df = pl.read_csv(url, dtypes={"date": pl.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    │
# └────────────┴───────┴───────┘

.groupby_dynamic()の第一引数にキー列、every=引数に時間間隔を指定します。

df.groupby_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.groupby_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.groupby_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   │
# └────────────┴───────┴───────┘

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
What you can do with signing up
292