結合
結合戦略
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 │
└─────────────────────┴───────┴───────┴───────┘