Skip to content

結合

結合戦略

Polarsは以下の結合戦略をサポートしており、 how 引数で指定できます:

戦略 説明
inner 両方のデータフレームで一致するキーを持つ行を返します。左右どちらかのデータフレームで一致しない行は破棄されます。
left 左側のデータフレームのすべての行を返します。右側のデータフレームで一致するものがない場合は、右側の列が null で埋められます。
outer 左右両方のデータフレームのすべての行を返します。一方のデータフレームで一致するものがない場合は、他方の列が null で埋められます。
outer_coalesce 左右両方のデータフレームのすべての行を返します。これは outer に似ていますが、キー列が結合されます。
cross 左側のデータフレームのすべての行と右側のデータフレームのすべての行のカルテシアン積を返します。重複する行は保持されます。AB を cross join した場合の行数は常に len(A) × len(B) になります。
semi 左側のデータフレームのキーが右側のデータフレームにも存在する行を返します。
anti 左側のデータフレームのキーが右側のデータフレームに存在しない行を返します。

内部結合

inner 結合は、結合キーが両方の DataFrame に存在する行のみを含む DataFrame を生成します。例えば、次の 2 つの DataFrame を考えてみましょう:

DataFrame

df_customers = pl.DataFrame(
    {
        "customer_id": [1, 2, 3],
        "name": ["Alice", "Bob", "Charlie"],
    }
)
print(df_customers)

DataFrame

let df_customers = df! (

    "customer_id" => &[1, 2, 3],
    "name" => &["Alice", "Bob", "Charlie"],
)?;

println!("{}", &df_customers);

shape: (3, 2)
┌─────────────┬─────────┐
│ customer_id ┆ name    │
│ ---         ┆ ---     │
│ i64         ┆ str     │
╞═════════════╪═════════╡
│ 1           ┆ Alice   │
│ 2           ┆ Bob     │
│ 3           ┆ Charlie │
└─────────────┴─────────┘

DataFrame

df_orders = pl.DataFrame(
    {
        "order_id": ["a", "b", "c"],
        "customer_id": [1, 2, 2],
        "amount": [100, 200, 300],
    }
)
print(df_orders)

DataFrame

let df_orders = df!(
        "order_id"=> &["a", "b", "c"],
        "customer_id"=> &[1, 2, 2],
        "amount"=> &[100, 200, 300],
)?;
println!("{}", &df_orders);

shape: (3, 3)
┌──────────┬─────────────┬────────┐
│ order_id ┆ customer_id ┆ amount │
│ ---      ┆ ---         ┆ ---    │
│ str      ┆ i64         ┆ i64    │
╞══════════╪═════════════╪════════╡
│ a        ┆ 1           ┆ 100    │
│ b        ┆ 2           ┆ 200    │
│ c        ┆ 2           ┆ 300    │
└──────────┴─────────────┴────────┘

注文と関連する顧客を持つ DataFrame を取得するには、customer_id 列で inner 結合を行います:

join

df_inner_customer_join = df_customers.join(df_orders, on="customer_id", how="inner")
print(df_inner_customer_join)

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 結合を使うことができます:

join

df_left_join = df_customers.join(df_orders, on="customer_id", how="left")
print(df_left_join)

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_id3 の顧客のフィールドが null になっていることに注目してください。この顧客には注文がないためです。

外部結合

outer 結合は、両方の DataFrame のすべての行を含む DataFrame を生成します。結合キーが存在しない場合、列は null になります。上記の 2 つの DataFrame に対して outer 結合を行うと、left 結合と似た DataFrame が生成されます:

join

df_outer_join = df_customers.join(df_orders, on="customer_id", how="outer")
print(df_outer_join)

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 結合と比較してみましょう:

join

df_outer_coalesce_join = df_customers.join(
    df_orders, on="customer_id", how="outer_coalesce"
)
print(df_outer_coalesce_join)

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_idcustomer_id_right の列が別々のままですが、outer_coalesce 結合では これらの列が単一の customer_id 列にマージされます。

クロス結合

クロス結合は、2つのDataFrameのカルテシアン積です。これは、左側のDataFrameの各行が右側のDataFrameの各行と結合されることを意味します。クロス結合は、2つのDataFrameの列のすべての組み合わせを持つDataFrameを作成するのに便利です。以下の2つのDataFrameを例に取ってみましょう。

DataFrame

df_colors = pl.DataFrame(
    {
        "color": ["red", "blue", "green"],
    }
)
print(df_colors)

DataFrame

let df_colors = df!(
        "color"=> &["red", "blue", "green"],
)?;
println!("{}", &df_colors);

shape: (3, 1)
┌───────┐
│ color │
│ ---   │
│ str   │
╞═══════╡
│ red   │
│ blue  │
│ green │
└───────┘

DataFrame

df_sizes = pl.DataFrame(
    {
        "size": ["S", "M", "L"],
    }
)
print(df_sizes)

DataFrame

let df_sizes = df!(
        "size"=> &["S", "M", "L"],
)?;
println!("{}", &df_sizes);

shape: (3, 1)
┌──────┐
│ size │
│ ---  │
│ str  │
╞══════╡
│ S    │
│ M    │
│ L    │
└──────┘

これで、クロス結合を使って、色とサイズのすべての組み合わせを含むDataFrameを作成できます:

join

df_cross_join = df_colors.join(df_sizes, how="cross")
print(df_cross_join)

join

let df_cross_join = df_colors
    .clone()
    .lazy()
    .cross_join(df_sizes.clone().lazy())
    .collect()?;
println!("{}", &df_cross_join);

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


innerleftoutercross結合の戦略は、データフレームライブラリの標準的なものです。以下では、あまり馴染みのないsemiantiasof結合の戦略についてより詳しく説明します。

半結合

semi 結合は、結合キーが右側のフレームにも存在する左側のフレームの行をすべて返します。次のようなシナリオを考えてみましょう。カーレンタル会社には、それぞれに一意の id を持つ車が登録された DataFrame があります。

DataFrame

df_cars = pl.DataFrame(
    {
        "id": ["a", "b", "c"],
        "make": ["ford", "toyota", "bmw"],
    }
)
print(df_cars)

DataFrame

let df_cars = df!(
        "id"=> &["a", "b", "c"],
        "make"=> &["ford", "toyota", "bmw"],
)?;
println!("{}", &df_cars);

shape: (3, 2)
┌─────┬────────┐
│ id  ┆ make   │
│ --- ┆ ---    │
│ str ┆ str    │
╞═════╪════════╡
│ a   ┆ ford   │
│ b   ┆ toyota │
│ c   ┆ bmw    │
└─────┴────────┘

この会社には、車両に実施された修理ジョブを示す別の DataFrame があります。

DataFrame

df_repairs = pl.DataFrame(
    {
        "id": ["c", "c"],
        "cost": [100, 200],
    }
)
print(df_repairs)

DataFrame

let df_repairs = df!(
        "id"=> &["c", "c"],
        "cost"=> &[100, 200],
)?;
println!("{}", &df_repairs);

shape: (2, 2)
┌─────┬──────┐
│ id  ┆ cost │
│ --- ┆ ---  │
│ str ┆ i64  │
╞═════╪══════╡
│ c   ┆ 100  │
│ c   ┆ 200  │
└─────┴──────┘

この質問に答えたいです: どの車が修理を受けたのでしょうか?

内部結合 (inner join) では、この質問に直接答えることはできません。なぜなら、複数回の修理ジョブを受けた車両について、複数の行が生成されるためです:

join

df_inner_join = df_cars.join(df_repairs, on="id", how="inner")
print(df_inner_join)

join

let df_inner_join = df_cars
    .clone()
    .lazy()
    .inner_join(df_repairs.clone().lazy(), col("id"), col("id"))
    .collect()?;
println!("{}", &df_inner_join);

shape: (2, 3)
┌─────┬──────┬──────┐
│ id  ┆ make ┆ cost │
│ --- ┆ ---  ┆ ---  │
│ str ┆ str  ┆ i64  │
╞═════╪══════╪══════╡
│ c   ┆ bmw  ┆ 100  │
│ c   ┆ bmw  ┆ 200  │
└─────┴──────┴──────┘

しかし、セミ結合 (semi join) を使えば、修理ジョブを受けた車両について、1行ずつ取得できます。

join

df_semi_join = df_cars.join(df_repairs, on="id", how="semi")
print(df_semi_join)

join

let df_semi_join = df_cars
    .clone()
    .lazy()
    .join(
        df_repairs.clone().lazy(),
        [col("id")],
        [col("id")],
        JoinArgs::new(JoinType::Semi),
    )
    .collect()?;
println!("{}", &df_semi_join);

shape: (1, 2)
┌─────┬──────┐
│ id  ┆ make │
│ --- ┆ ---  │
│ str ┆ str  │
╞═════╪══════╡
│ c   ┆ bmw  │
└─────┴──────┘

逆結合

この例を続けると、別の質問として次のようなものが考えられます: どの車にも修理が行われていないのはどれですか? 逆結合を使うと、df_repairs DataFrame に存在しない id を持つ df_cars の車を示す DataFrame が得られます。

join

df_anti_join = df_cars.join(df_repairs, on="id", how="anti")
print(df_anti_join)

join

let df_anti_join = df_cars
    .clone()
    .lazy()
    .join(
        df_repairs.clone().lazy(),
        [col("id")],
        [col("id")],
        JoinArgs::new(JoinType::Anti),
    )
    .collect()?;
println!("{}", &df_anti_join);

shape: (2, 2)
┌─────┬────────┐
│ id  ┆ make   │
│ --- ┆ ---    │
│ str ┆ str    │
╞═════╪════════╡
│ a   ┆ ford   │
│ b   ┆ toyota │
└─────┴────────┘

直前の引用

asof 結合は左結合のようなものですが、等しいキーではなく最も近いキーでマッチさせます。 Polars では join_asof メソッドを使って asof 結合を行うことができます。

次のようなシナリオを考えましょう: 株式仲介業者には df_trades という取引記録の DataFrame があります。

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)

DataFrame

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 もあります。

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)

DataFrame

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" を指定して事前の正確な結合を行う必要があります。

join_asof

df_asof_join = df_trades.join_asof(df_quotes, on="time", by="stock")
print(df_asof_join)

join_asof

let df_asof_join = df_trades.join_asof_by(
    &df_quotes,
    "time",
    "time",
    ["stock"],
    ["stock"],
    AsofStrategy::Backward,
    None,
)?;
println!("{}", &df_asof_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   ┆ 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  │
└─────────────────────┴───────┴───────┴───────┘