結合
結合戦略
Polarsは以下の結合戦略をサポートしており、 how 引数で指定できます:
| 戦略 | 説明 | 
|---|---|
| inner | 両方のデータフレームで一致するキーを持つ行を返します。左右どちらかのデータフレームで一致しない行は破棄されます。 | 
| left | 左側のデータフレームのすべての行を返します。右側のデータフレームで一致するものがない場合は、右側の列が null で埋められます。 | 
| outer | 左右両方のデータフレームのすべての行を返します。一方のデータフレームで一致するものがない場合は、他方の列が null で埋められます。 | 
| outer_coalesce | 左右両方のデータフレームのすべての行を返します。これは outerに似ていますが、キー列が結合されます。 | 
| cross | 左側のデータフレームのすべての行と右側のデータフレームのすべての行のカルテシアン積を返します。重複する行は保持されます。 AとBを cross join した場合の行数は常にlen(A) × len(B)になります。 | 
| semi | 左側のデータフレームのキーが右側のデータフレームにも存在する行を返します。 | 
| anti | 左側のデータフレームのキーが右側のデータフレームに存在しない行を返します。 | 
内部結合
inner 結合は、結合キーが両方の DataFrame に存在する行のみを含む DataFrame を生成します。例えば、次の 2 つの DataFrame を考えてみましょう:
shape: (3, 2)
┌─────────────┬─────────┐
│ customer_id ┆ name    │
│ ---         ┆ ---     │
│ i64         ┆ str     │
╞═════════════╪═════════╡
│ 1           ┆ Alice   │
│ 2           ┆ Bob     │
│ 3           ┆ Charlie │
└─────────────┴─────────┘
shape: (3, 3)
┌──────────┬─────────────┬────────┐
│ order_id ┆ customer_id ┆ amount │
│ ---      ┆ ---         ┆ ---    │
│ str      ┆ i64         ┆ i64    │
╞══════════╪═════════════╪════════╡
│ a        ┆ 1           ┆ 100    │
│ b        ┆ 2           ┆ 200    │
│ c        ┆ 2           ┆ 300    │
└──────────┴─────────────┴────────┘
注文と関連する顧客を持つ DataFrame を取得するには、customer_id 列で inner 結合を行います:
df_inner_customer_join = df_customers.join(df_orders, on="customer_id", how="inner")
print(df_inner_customer_join)
let df_inner_customer_join = df_customers
    .clone()
    .lazy()
    .join(
        df_orders.clone().lazy(),
        [col("customer_id")],
        [col("customer_id")],
        JoinArgs::new(JoinType::Inner),
    )
    .collect()?;
println!("{}", &df_inner_customer_join);
shape: (3, 4)
┌─────────────┬───────┬──────────┬────────┐
│ customer_id ┆ name  ┆ order_id ┆ amount │
│ ---         ┆ ---   ┆ ---      ┆ ---    │
│ i64         ┆ str   ┆ str      ┆ i64    │
╞═════════════╪═══════╪══════════╪════════╡
│ 1           ┆ Alice ┆ a        ┆ 100    │
│ 2           ┆ Bob   ┆ b        ┆ 200    │
│ 2           ┆ Bob   ┆ c        ┆ 300    │
└─────────────┴───────┴──────────┴────────┘
左結合
left 結合は、左側の DataFrame のすべての行と、右側の DataFrame の結合キーが左側の DataFrame に存在する行のみを含む DataFrame を生成します。上記の例を使って、すべての顧客とそれらの注文(注文の有無に関わらず)を含む DataFrame を作成する場合は、left 結合を使うことができます:
df_left_join = df_customers.join(df_orders, on="customer_id", how="left")
print(df_left_join)
let df_left_join = df_customers
    .clone()
    .lazy()
    .join(
        df_orders.clone().lazy(),
        [col("customer_id")],
        [col("customer_id")],
        JoinArgs::new(JoinType::Left),
    )
    .collect()?;
println!("{}", &df_left_join);
shape: (4, 4)
┌─────────────┬─────────┬──────────┬────────┐
│ customer_id ┆ name    ┆ order_id ┆ amount │
│ ---         ┆ ---     ┆ ---      ┆ ---    │
│ i64         ┆ str     ┆ str      ┆ i64    │
╞═════════════╪═════════╪══════════╪════════╡
│ 1           ┆ Alice   ┆ a        ┆ 100    │
│ 2           ┆ Bob     ┆ b        ┆ 200    │
│ 2           ┆ Bob     ┆ c        ┆ 300    │
│ 3           ┆ Charlie ┆ null     ┆ null   │
└─────────────┴─────────┴──────────┴────────┘
customer_id が 3 の顧客のフィールドが null になっていることに注目してください。この顧客には注文がないためです。
外部結合
outer 結合は、両方の DataFrame のすべての行を含む DataFrame を生成します。結合キーが存在しない場合、列は null になります。上記の 2 つの DataFrame に対して outer 結合を行うと、left 結合と似た DataFrame が生成されます:
df_outer_join = df_customers.join(df_orders, on="customer_id", how="outer")
print(df_outer_join)
let df_outer_join = df_customers
    .clone()
    .lazy()
    .join(
        df_orders.clone().lazy(),
        [col("customer_id")],
        [col("customer_id")],
        JoinArgs::new(JoinType::Outer),
    )
    .collect()?;
println!("{}", &df_outer_join);
shape: (4, 5)
┌─────────────┬─────────┬──────────┬───────────────────┬────────┐
│ customer_id ┆ name    ┆ order_id ┆ customer_id_right ┆ amount │
│ ---         ┆ ---     ┆ ---      ┆ ---               ┆ ---    │
│ i64         ┆ str     ┆ str      ┆ i64               ┆ i64    │
╞═════════════╪═════════╪══════════╪═══════════════════╪════════╡
│ 1           ┆ Alice   ┆ a        ┆ 1                 ┆ 100    │
│ 2           ┆ Bob     ┆ b        ┆ 2                 ┆ 200    │
│ 2           ┆ Bob     ┆ c        ┆ 2                 ┆ 300    │
│ 3           ┆ Charlie ┆ null     ┆ null              ┆ null   │
└─────────────┴─────────┴──────────┴───────────────────┴────────┘
外部結合とコアレス
outer_coalesce 結合は、outer 結合のように両方の DataFrames からすべての行を結合しますが、結合キーの値をコアレスして単一の列にマージします。これにより、キー列のNULLを可能な限り避けて、結合キーの統一された表示を確保します。前述の2つの DataFrames を使って、outer 結合と比較してみましょう:
df_outer_coalesce_join = df_customers.join(
    df_orders, on="customer_id", how="outer_coalesce"
)
print(df_outer_coalesce_join)
let df_outer_join = df_customers
    .clone()
    .lazy()
    .join(
        df_orders.clone().lazy(),
        [col("customer_id")],
        [col("customer_id")],
        JoinArgs::new(JoinType::Outer),
    )
    .collect()?;
println!("{}", &df_outer_join);
shape: (4, 4)
┌─────────────┬─────────┬──────────┬────────┐
│ customer_id ┆ name    ┆ order_id ┆ amount │
│ ---         ┆ ---     ┆ ---      ┆ ---    │
│ i64         ┆ str     ┆ str      ┆ i64    │
╞═════════════╪═════════╪══════════╪════════╡
│ 1           ┆ Alice   ┆ a        ┆ 100    │
│ 2           ┆ Bob     ┆ b        ┆ 200    │
│ 2           ┆ Bob     ┆ c        ┆ 300    │
│ 3           ┆ Charlie ┆ null     ┆ null   │
└─────────────┴─────────┴──────────┴────────┘
outer 結合では customer_id と customer_id_right の列が別々のままですが、outer_coalesce 結合では これらの列が単一の customer_id 列にマージされます。
クロス結合
クロス結合は、2つのDataFrameのカルテシアン積です。これは、左側のDataFrameの各行が右側のDataFrameの各行と結合されることを意味します。クロス結合は、2つのDataFrameの列のすべての組み合わせを持つDataFrameを作成するのに便利です。以下の2つのDataFrameを例に取ってみましょう。
shape: (3, 1)
┌───────┐
│ color │
│ ---   │
│ str   │
╞═══════╡
│ red   │
│ blue  │
│ green │
└───────┘
shape: (3, 1)
┌──────┐
│ size │
│ ---  │
│ str  │
╞══════╡
│ S    │
│ M    │
│ L    │
└──────┘
これで、クロス結合を使って、色とサイズのすべての組み合わせを含むDataFrameを作成できます:
shape: (9, 2)
┌───────┬──────┐
│ color ┆ size │
│ ---   ┆ ---  │
│ str   ┆ str  │
╞═══════╪══════╡
│ red   ┆ S    │
│ red   ┆ M    │
│ red   ┆ L    │
│ blue  ┆ S    │
│ blue  ┆ M    │
│ blue  ┆ L    │
│ green ┆ S    │
│ green ┆ M    │
│ green ┆ L    │
└───────┴──────┘
inner、left、outer、cross結合の戦略は、データフレームライブラリの標準的なものです。以下では、あまり馴染みのないsemi、anti、asof結合の戦略についてより詳しく説明します。
半結合
semi 結合は、結合キーが右側のフレームにも存在する左側のフレームの行をすべて返します。次のようなシナリオを考えてみましょう。カーレンタル会社には、それぞれに一意の id を持つ車が登録された DataFrame があります。
shape: (3, 2)
┌─────┬────────┐
│ id  ┆ make   │
│ --- ┆ ---    │
│ str ┆ str    │
╞═════╪════════╡
│ a   ┆ ford   │
│ b   ┆ toyota │
│ c   ┆ bmw    │
└─────┴────────┘
この会社には、車両に実施された修理ジョブを示す別の DataFrame があります。
shape: (2, 2)
┌─────┬──────┐
│ id  ┆ cost │
│ --- ┆ ---  │
│ str ┆ i64  │
╞═════╪══════╡
│ c   ┆ 100  │
│ c   ┆ 200  │
└─────┴──────┘
この質問に答えたいです: どの車が修理を受けたのでしょうか?
内部結合 (inner join) では、この質問に直接答えることはできません。なぜなら、複数回の修理ジョブを受けた車両について、複数の行が生成されるためです:
shape: (2, 3)
┌─────┬──────┬──────┐
│ id  ┆ make ┆ cost │
│ --- ┆ ---  ┆ ---  │
│ str ┆ str  ┆ i64  │
╞═════╪══════╪══════╡
│ c   ┆ bmw  ┆ 100  │
│ c   ┆ bmw  ┆ 200  │
└─────┴──────┴──────┘
しかし、セミ結合 (semi join) を使えば、修理ジョブを受けた車両について、1行ずつ取得できます。
shape: (1, 2)
┌─────┬──────┐
│ id  ┆ make │
│ --- ┆ ---  │
│ str ┆ str  │
╞═════╪══════╡
│ c   ┆ bmw  │
└─────┴──────┘
逆結合
この例を続けると、別の質問として次のようなものが考えられます: どの車にも修理が行われていないのはどれですか? 逆結合を使うと、df_repairs DataFrame に存在しない id を持つ df_cars の車を示す DataFrame が得られます。
shape: (2, 2)
┌─────┬────────┐
│ id  ┆ make   │
│ --- ┆ ---    │
│ str ┆ str    │
╞═════╪════════╡
│ a   ┆ ford   │
│ b   ┆ toyota │
└─────┴────────┘
直前の引用
asof 結合は左結合のようなものですが、等しいキーではなく最も近いキーでマッチさせます。
Polars では join_asof メソッドを使って asof 結合を行うことができます。
次のようなシナリオを考えましょう: 株式仲介業者には df_trades という取引記録の DataFrame があります。
df_trades = pl.DataFrame(
    {
        "time": [
            datetime(2020, 1, 1, 9, 1, 0),
            datetime(2020, 1, 1, 9, 1, 0),
            datetime(2020, 1, 1, 9, 3, 0),
            datetime(2020, 1, 1, 9, 6, 0),
        ],
        "stock": ["A", "B", "B", "C"],
        "trade": [101, 299, 301, 500],
    }
)
print(df_trades)
use chrono::prelude::*;
let df_trades = df!(
    "time"=> &[
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 1, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 1, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 3, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 6, 0).unwrap(),
        ],
        "stock"=> &["A", "B", "B", "C"],
        "trade"=> &[101, 299, 301, 500],
)?;
println!("{}", &df_trades);
shape: (4, 3)
┌─────────────────────┬───────┬───────┐
│ time                ┆ stock ┆ trade │
│ ---                 ┆ ---   ┆ ---   │
│ datetime[μs]        ┆ str   ┆ i64   │
╞═════════════════════╪═══════╪═══════╡
│ 2020-01-01 09:01:00 ┆ A     ┆ 101   │
│ 2020-01-01 09:01:00 ┆ B     ┆ 299   │
│ 2020-01-01 09:03:00 ┆ B     ┆ 301   │
│ 2020-01-01 09:06:00 ┆ C     ┆ 500   │
└─────────────────────┴───────┴───────┘
この仲介業者には、これらの株式の価格情報を示す df_quotes という別の DataFrame もあります。
df_quotes = pl.DataFrame(
    {
        "time": [
            datetime(2020, 1, 1, 9, 0, 0),
            datetime(2020, 1, 1, 9, 2, 0),
            datetime(2020, 1, 1, 9, 4, 0),
            datetime(2020, 1, 1, 9, 6, 0),
        ],
        "stock": ["A", "B", "C", "A"],
        "quote": [100, 300, 501, 102],
    }
)
print(df_quotes)
let df_quotes = df!(
        "time"=> &[
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 0, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 2, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 4, 0).unwrap(),
    NaiveDate::from_ymd_opt(2020, 1, 1).unwrap().and_hms_opt(9, 6, 0).unwrap(),
        ],
        "stock"=> &["A", "B", "C", "A"],
        "quote"=> &[100, 300, 501, 102],
)?;
println!("{}", &df_quotes);
shape: (4, 3)
┌─────────────────────┬───────┬───────┐
│ time                ┆ stock ┆ quote │
│ ---                 ┆ ---   ┆ ---   │
│ datetime[μs]        ┆ str   ┆ i64   │
╞═════════════════════╪═══════╪═══════╡
│ 2020-01-01 09:00:00 ┆ A     ┆ 100   │
│ 2020-01-01 09:02:00 ┆ B     ┆ 300   │
│ 2020-01-01 09:04:00 ┆ C     ┆ 501   │
│ 2020-01-01 09:06:00 ┆ A     ┆ 102   │
└─────────────────────┴───────┴───────┘
各取引について、取引の直前に提示された最新の価格情報を表示する DataFrame を作成したいと思います。これを実現するには join_asof を使います(デフォルトの strategy = "backward" を使用)。
株式ごとに取引と価格情報が正しくマッチするよう、by="stock" を指定して事前の正確な結合を行う必要があります。
shape: (4, 4)
┌─────────────────────┬───────┬───────┬───────┐
│ time                ┆ stock ┆ trade ┆ quote │
│ ---                 ┆ ---   ┆ ---   ┆ ---   │
│ datetime[μs]        ┆ str   ┆ i64   ┆ i64   │
╞═════════════════════╪═══════╪═══════╪═══════╡
│ 2020-01-01 09:01:00 ┆ A     ┆ 101   ┆ 100   │
│ 2020-01-01 09:01:00 ┆ B     ┆ 299   ┆ null  │
│ 2020-01-01 09:03:00 ┆ B     ┆ 301   ┆ 300   │
│ 2020-01-01 09:06:00 ┆ C     ┆ 500   ┆ 501   │
└─────────────────────┴───────┴───────┴───────┘
取引と価格情報の間に一定の時間範囲を設けたい場合は、tolerance 引数を指定できます。ここでは取引の 1 分前までの価格情報を結合したいので、tolerance = "1m" と設定しています。
df_asof_tolerance_join = df_trades.join_asof(
    df_quotes, on="time", by="stock", tolerance="1m"
)
print(df_asof_tolerance_join)
shape: (4, 4)
┌─────────────────────┬───────┬───────┬───────┐
│ time                ┆ stock ┆ trade ┆ quote │
│ ---                 ┆ ---   ┆ ---   ┆ ---   │
│ datetime[μs]        ┆ str   ┆ i64   ┆ i64   │
╞═════════════════════╪═══════╪═══════╪═══════╡
│ 2020-01-01 09:01:00 ┆ A     ┆ 101   ┆ 100   │
│ 2020-01-01 09:01:00 ┆ B     ┆ 299   ┆ null  │
│ 2020-01-01 09:03:00 ┆ B     ┆ 301   ┆ 300   │
│ 2020-01-01 09:06:00 ┆ C     ┆ 500   ┆ null  │
└─────────────────────┴───────┴───────┴───────┘