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

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

https://github.com/pola-rs/polars

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

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

- [Github](https://github.com/pola-rs/polars)
- [公式APIリファレンス](https://docs.pola.rs/api/python/stable/reference/index.html)
- [公式ガイド](https://docs.pola.rs)

本記事の内容はバージョン1.1.0 (2024/7/7)で確認しています。それ以前の挙動についてはこの記事の編集履歴を参照してください。

## 基礎

`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.String`                 | `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()`関数](https://docs.pola.rs/api/python/stable/reference/api/polars.read_csv.html#polars-read-csv)を使います。第一引数としてファイルパスを受け取り、データフレームを返します。

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

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

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

なお、polarsには[`scan_csv()`](https://docs.pola.rs/api/python/stable/reference/api/polars.scan_csv.html#polars-scan-csv)という関数もあります。受け取る引数は`read_csv()`関数とほぼ同じです。

```python
pl.scan_csv(file)
```

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

### 他の配列から変換

[`pl.DataFrame()`コンストラクタ](https://docs.pola.rs/api/python/stable/reference/dataframe/index.html#dataframe)を使ってデータフレームを作成できます。このとき`schema`引数を渡すと列名を設定できます。`schema=`引数は列名のリスト、または`｛列名：データ型｝`の辞書で指定します。

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

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

```python
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()`メソッド](https://docs.pola.rs/api/python/stable/reference/api/polars.DataFrame.write_csv.html#polars-dataframe-write-csv)を使います。なお、ヘッダーをファイルの1行目として出力しない場合は`include_header=False`を指定します。

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

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

```python: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行が表示されます。

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

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

### `.glimpse()`メソッド

[`.glimpse()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.glimpse.html#polars-dataframe-glimpse)では、文字列として、各列の先頭の要素10個が表示されます。列数が多い場合、`print()`よりもこちらのほうが見やすいとされています。

```python
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()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.describe.html#polars-dataframe-describe)を使用することができます。表示される情報は、データ数・欠損値の数・平均値・標準偏差・最小値・25%tile値・中央値・75%tile値・最大値です。

```python
df.describe()
# shape: (9, 5)
# ┌────────────┬──────────┬──────────┬─────────────────────┬────────┐
# │ statistic  ┆ 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      ┆ 2022-04-01 00:00:00 ┆ 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      ┆ 2022-04-01 00:00:00 ┆ null   │
# │ 50%        ┆ 3.0      ┆ 3.0      ┆ 2022-04-01 00:00:00 ┆ null   │
# │ 75%        ┆ 3.0      ┆ 3.0      ┆ 2022-04-01 00:00:00 ┆ null   │
# │ max        ┆ 4.0      ┆ 4.0      ┆ 2022-04-01 00:00:00 ┆ train  │
# └────────────┴──────────┴──────────┴─────────────────────┴────────┘
```

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

### `[]`を使った行の選択

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

```python
# 一行選択
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()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.head.html#polars-dataframe-head)と[`.tail()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.tail.html#polars-dataframe-tail)はpandasと同様、最初・最後の数行（デフォルトでは5行）を抽出します。

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

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

### 行フィルタリング（`.filter()`）

条件にもとづいて特定の行を抽出する場合は[`.filter()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.filter.html#polars-dataframe-filter)を用います。pandasの`df[df["column"] > 3]`のような書き方はできません。

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

```python
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条件は`|`でつなぎます。`&`や`|`は計算優先順位が高いので、括弧`()`の位置に注意しましょう。

```python
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条件として解釈されます。

```python
# 上の例と等価
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) == `として解釈されます。

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


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

```python
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()`メソッドに相当します。

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

### ユニーク行・重複行

[`.unique()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.unique.html#polars-dataframe-unique)は、pandasの`.drop_duplicates()`に相当し、値が重複しているデータを削除することができます。

- `subset=`引数：識別する列を指定します。デフォルトでは、全ての列の値が一致している行のみが削除されます。
- `keep=`引数：デフォルトの`any`は、重複行のうち予測不可能な **いずれか1行** を残します。`keep="first"`を指定すると重複行のうち最初に出現する行が残されます。`keep="last"`を指定すると最後に出現する行が残ります。`keep="none"`を指定すると重複行をすべて削除します。

```python
rng = np.random.default_rng(0)
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.636962 │
# │ one   ┆ y   ┆ 0.269787 │
# │ two   ┆ x   ┆ 0.040974 │
# │ two   ┆ y   ┆ 0.016528 │
# │ three ┆ x   ┆ 0.912756 │
# │ four  ┆ x   ┆ 0.606636 │
# └───────┴─────┴──────────┘

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

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

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

重複するかどうかの真偽値は[`.is_duplicated()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.is_duplicated.html#polars-dataframe-is-duplicated)で確認でき、逆にユニークかどうかは[`.is_unique()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.is_unique.html#polars-dataframe-is-unique)で確認できますが、`subset=`や`keep=`引数を渡すことはできません。

```python
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`引数と同じ効果が得られます。

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

## 列の選択と追加

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

データフレーム内の特定のシリーズ（1列）を選択する場合は、[`.get_column()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.get_column.html#polars-dataframe-get-column)に列名を渡します（pandasのように`df[col]`と書くことも可能です）。

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

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

[`.get_columns()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.get_columns.html#polars-dataframe-get-columns)は、データフレームの全ての列を分解し、シリーズのリストに変換します。これを用いると、「左からX番目の列」（Xは0はじまり）のシリーズを取得することができます。

```python
type(df.get_columns())
# list<class 'list'>

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

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

```python
df.get_column_index("Float")
# 1
```

### `.select()`を使った列選択

データフレーム内の（1つ以上の）特定の列を選択する場合は、[`.select()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.select.html#polars-dataframe-select)を用います。

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

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

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

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

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

```python
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()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.with_columns.html#polars-dataframe-with-columns)を使います（pandasの`.assign()`に似ています）。

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

```python
# 新しいシリーズの作成を伴う操作
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        │
# └─────────┴───────┴─────────────────────┴────────┴──────────┘
```

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

```python
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()`にエクスプレッションを複数渡すと、一度に複数の列を追加できます。

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

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

```python
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_index()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.with_row_index.html#polars-dataframe-with-row-index)は、左端に0始まりの整数列「index」（列名は`name=`引数で指定可能）を追加します。感覚的にpandasの`.reset_index()`に似ているかもしれません。

```python
df.with_row_count()
# shape: (4, 5)
# ┌───────┬─────────┬───────┬─────────────────────┬────────┐
# │ index ┆ 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()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.arange.html#polars-arange)は、`np.arange()`や`pd.RangeIndex()`に相当する、等差数列を作成するエクスプレッションです。

```python
df.select(pl.arange(0, df.height).alias("index"), pl.all())

# shape: (4, 5)
# ┌───────┬─────────┬───────┬─────────────────────┬────────┐
# │ index ┆ 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  │
# └───────┴─────────┴───────┴─────────────────────┴────────┘
```

## 基礎的な操作と演算

### 四則演算

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

```python
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()`メソッドとエクスプレッションを使います。

```python
df_num.get_column("A") ** 2
# shape: (6,)
# Series: 'A' [i64]
# [
#     64
#     4
#     0
#     64
#     25
#     49
# ]

df_num.select(pl.all().pow(2))
# shape: (6, 3)
# ┌─────┬─────┬─────┐
# │ A   ┆ B   ┆ C   │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ i64 ┆ i64 │
# ╞═════╪═════╪═════╡
# │ 64  ┆ 36  ┆ 25  │
# │ 4   ┆ 9   ┆ 0   │
# │ 0   ┆ 0   ┆ 1   │
# │ 64  ┆ 36  ┆ 81  │
# │ 25  ┆ 36  ┆ 81  │
# │ 49  ┆ 36  ┆ 25  │
# └─────┴─────┴─────┘
```

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

```python
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()`メソッドとエクスプレッションを使います）。シリーズは直接数値と比較可能です。

```python
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
# ]
```

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

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

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

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

### 集計関数

[`.sum()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.sum.html#polars-dataframe-sum)や[`.mean()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.mean.html#polars-dataframe-mean)等の関数は列（垂直）方向に適用できます。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.sum_horizontal.html#polars-dataframe-sum-horizontal)等の“_horizontal”メソッドを使うか、より柔軟に複雑な計算を行う場合は[`.fold()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.fold.html)を使います。後者はおおむねPython標準の[`functools.reduce()`](https://docs.python.org/ja/3/library/functools.html#functools.reduce)に対応します。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/series/api/polars.Series.value_counts.html#polars-series-value-counts)はpandasと同様に扱うことができます。なおデータフレームには`.value_counts()`メソッドはありません（同等の結果は`.groupby(pl.all()).len()`で得ることができます）。

```python
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()`メソッドを続けましょう）。

```python
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()`は、構造化配列を返します。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/expressions/col.html#polars-col)に列名を渡すことで行います。

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

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

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

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

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

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

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

[`pl.all()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.all.html#polars-all)、または`pl.col("*")`ですべての列を選択します。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.exclude.html#polars-exclude)は指定した列以外のすべてを選択します。`pl.col()`と同様にリストや正規表現もつかうことができます。

```python
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`](https://docs.pola.rs/api/python/stable/reference/selectors.html)を使うとさらに複雑な条件を直感的に指定できます。

### 列名の変更

列名を任意の文字列に変更する場合は[`.alias()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.alias.html#polars-expr-alias)を使います。polarsでは列名の重複を許さないのでよく使います。`.alias()`は`pl.col()`で二列以上選択している場合は使用できません。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.name.prefix.html#polars-expr-name-prefix)と[`.name.suffix()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.name.suffix.html#polars-expr-name-suffix)で列名の前後に任意の文字列をつなげることができます。

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

### ソート・並び替え

ソートは[`.sort()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.sort.html#polars-expr-sort)を使います。

```python
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()`等で複数列選択していた場合でも、ソートは **列ごとに独立して** 行われるので注意してください。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.sort_by.html#polars-expr-sort-by)を使用します。キーを複数指定することもできます。

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

```python
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()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.reverse.html#polars-expr-reverse)は、列を上下逆に並び替えます（つまりスライス記法でいう`[::-1]`）。

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

### 集計計算

列選択後、[`.sum()`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.sum.html#polars-expr-sum)で総和を計算します。

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

`pl.col(列名).sum()`は、省略して[`pl.sum(列名)`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.sum.html#polars-sum)と書くこともできます。

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

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

```python
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()`）。

```python
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の場合)`](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.when.html#polars-when)という特殊なチェーンメソッドによって行います。

```python
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()`とします。

```python
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`の場合は次の列で上書きします。

```python
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()`の異名で、条件文を与えると行フィルタリングします。

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

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

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

[`pl.concat()`関数](https://docs.pola.rs/api/python/stable/reference/api/polars.concat.html#polars-concat)を使って、複数のデータフレームを結合することができます。

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

```python
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`で埋められます。

```python
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`で埋められます。

```python
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つのデータフレームに共通して存在するキーのみを保持）になります。

```python
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="right"`を指定します。

```python
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="right")
# shape: (4, 6)
# ┌──────┬──────┬──────┬──────┬─────┬─────┐
# │ A    ┆ B    ┆ key1 ┆ key2 ┆ C   ┆ D   │
# │ ---  ┆ ---  ┆ ---  ┆ ---  ┆ --- ┆ --- │
# │ str  ┆ str  ┆ str  ┆ str  ┆ str ┆ str │
# ╞══════╪══════╪══════╪══════╪═════╪═════╡
# │ A0   ┆ B0   ┆ K0   ┆ K0   ┆ C0  ┆ D0  │
# │ A2   ┆ B2   ┆ K1   ┆ K0   ┆ C1  ┆ D1  │
# │ A2   ┆ B2   ┆ K1   ┆ K0   ┆ C2  ┆ D2  │
# │ null ┆ null ┆ K2   ┆ K0   ┆ C3  ┆ D3  │
# └──────┴──────┴──────┴──────┴─────┴─────┘
```

完全外部結合は`how="full"`を指定します。このとき`coalesce=`引数によって結果が大きく違うので注意してください。

- `coalesce=False`（デフォルト）の場合、左右フレームのキー列が維持された状態で結合されます。列名が重複する場合は`suffix=`引数で右フレームのキー列名に接尾辞を付加できます（デフォルトは`"_right"`）。
- `coalesce=True`の場合、左右フレームのキー列を融合させた新しいキー列を作ります。

```python
left.join(right, on=["key1", "key2"], how="full")
# 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="full", coalesce=True)
# 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"`）も可能です。

```python
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  │
# │ K0   ┆ K1   ┆ A1  ┆ B1  ┆ K0         ┆ K0         ┆ C0  ┆ D0  │
# │ …    ┆ …    ┆ …   ┆ …   ┆ …          ┆ …          ┆ …   ┆ …   │
# │ K1   ┆ K0   ┆ A2  ┆ B2  ┆ 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"`は右のデータフレームに存在しないキーを持つ行だけを残します。

```python
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()`メソッド](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.group_by.html#polars-dataframe-group-by)を使います。pandasの`.groupby()`と同様に、列名または列名のリストをキーとして渡すことができます。結果のデータフレームは、pandasではグループキーに基づいてソートされていますが、polarsでは**行の並び順はランダム**です（同じコードでも計算するたびに変わります）。

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

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

```python
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ではグループキーに`NaN`・`null`がある行も保持されます（pandasはデフォルトではグループキーが`nan`・`NA`の行は削除されます）。

```python
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のように、`(グループ名のタプル, データフレーム)`のタプルを取得できます。

```python
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`オブジェクトのメソッドを使います。

```python
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").len()
# 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()`などで適宜列名を調整する必要があります。

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

### ピボットテーブル

[`DataFrame.pivot()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.pivot.html#polars-dataframe-pivot)でピボットテーブルを作成します。第1引数`on=`に、横持ちにさせたい列を指定します。縦持ちとして維持する列は`index=`引数、値にする列は`value=`引数、集計関数は`aggregate_function=`引数として渡します。pandasの`DataFrame.pivot_table()`に相当します。

```python
pivot_table = df.pivot(on="B", index="A", values="C", aggregate_function="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 │
# └─────┴──────────┴──────────┴──────────┘
```

なお、ピボットテーブルを戻す場合は[`.unpivot()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.unpivot.html#polars-dataframe-unpivot)を用います。pandasの`.melt()`（あるいは`.unstack()`）に相当します。

```python
pivot_table.unpivot(
    index="A", on=["one", "two", "three"], variable_name="B", value_name="C"
)
# shape: (6, 3)
# ┌─────┬───────┬──────────┐
# │ A   ┆ B     ┆ C        │
# │ --- ┆ ---   ┆ ---      │
# │ 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`を持つことができ、浮動小数データ型のシリーズでは`null`と`NaN`が共存できます。

```python
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()`メソッドで得られます。これらはどちらもシリーズまたはエクスプレッションのみに存在するメソッドで、データフレームのメソッドではありません。

```python
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`の数を集計します。

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

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

```python
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`が維持されます。`null`と`NaN`の組み合わせで演算を実行した場合は`null`が優先されます（後述する集計関数との違いに注意）。

```python
df_missing.with_columns(
    A_plus_B=pl.col("A") + pl.col("B"),  # 加算演算によるAとBの和
    sum_A_B=pl.sum_horizontal("A", "B"),  # 集計関数によるAとBの和
)
# shape: (6, 4)
# ┌──────┬──────┬──────────┬─────────┐
# │ A    ┆ B    ┆ A_plus_B ┆ sum_A_B │
# │ ---  ┆ ---  ┆ ---      ┆ ---     │
# │ i64  ┆ f64  ┆ f64      ┆ f64     │
# ╞══════╪══════╪══════════╪═════════╡
# │ 1    ┆ NaN  ┆ NaN      ┆ NaN     │
# │ 2    ┆ 4.0  ┆ 6.0      ┆ 6.0     │
# │ null ┆ 3.0  ┆ null     ┆ 3.0     │
# │ 4    ┆ 2.0  ┆ 6.0      ┆ 6.0     │
# │ null ┆ NaN  ┆ null     ┆ NaN     │
# │ null ┆ null ┆ null     ┆ 0.0     │
# └──────┴──────┴──────────┴─────────┘
```

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

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

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

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

df_missing.describe()
# shape: (9, 3)
# ┌────────────┬──────────┬─────┐
# │ statistic  ┆ 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()`メソッドを使います。

```python
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()`にはエクスプレッションやシリーズを渡すことができます。

``` python
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()`メソッドは特定のキーワードを与えることもできます。平均値で穴埋めする場合は`strategy="mean"`を指定します。

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

`strategy=`引数は、`"mean"`の他、`"forward"`・`"backward"`・`"min"`・`"max"`・`"zero"`・`"one"`を受け入れます。

## テキストデータを扱う

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

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

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

```python
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()`の方が高速です。

```python
pl.DataFrame({"a": ["Tokyo", "東京", "Cafe", "Café", "Café", 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         │
# │ Café  ┆ 6         ┆ 5         │
# │ null  ┆ null      ┆ null      │
# └───────┴───────────┴───────────┘
```

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

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

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

```python
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の文字列メソッドにはいわゆる`match`や`fullmatch`はありませんので、この`.str.contains()`メソッドで代用してください。

```python
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()`メソッドを使います。

```python
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"`など）については[こちら](https://docs.rs/chrono/latest/chrono/format/strftime/)を参照してください。

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

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

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

[`pl.date_range()`関数](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.date_range.html#polars-date-range)と[`pl.datetime_range()`関数](https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.datetime_range.html#polars-datetime-range)は、pandasの`pd.date_range()`関数に対応します。

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

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

pl.datetime_range(
    datetime.datetime(2022, 1, 1),
    datetime.datetime(2022, 1, 10),
    datetime.timedelta(days=1, hours=12),
    eager=True,
)
# shape: (7,)
# Series: 'literal' [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
# ]
```

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

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

```python
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
# String

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と違い属性アクセスではありません）。

```python
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  ┆ i8    ┆ i8  ┆ i8   ┆ i8     ┆ i8     ┆ 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()`にフォーマット文字列を渡します。

```python
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()`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.group_by_dynamic.html#polars-dataframe-group-by-dynamic)で時系列に基づいたグループ集計を行います。pandasの`.resample()`に相当します。

```python
url = "https://raw.githubusercontent.com/nkmk/python-snippets/77f7a51e776077396fd8f8f8c1801d54e9854479/notebook/data/src/sample_date.csv"
df = pl.read_csv(url, schema_overrides={"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    │
# │ 2017-12-05 ┆ 65    ┆ 85    │
# │ …          ┆ …     ┆ …     │
# │ 2017-12-29 ┆ 21    ┆ 8     │
# │ 2018-01-03 ┆ 98    ┆ 76    │
# │ 2018-01-08 ┆ 48    ┆ 64    │
# │ 2018-01-19 ┆ 18    ┆ 48    │
# │ 2018-01-23 ┆ 86    ┆ 70    │
# └────────────┴───────┴───────┘
```

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

```python
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日間の値を月毎に合計するという意味になります。

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