{ "cells": [ { "cell_type": "markdown", "id": "2239e303-b5eb-4c03-bf1a-9817e6c5153a", "metadata": {}, "source": [ "# 複数のデータフレームの結合" ] }, { "cell_type": "markdown", "id": "6fd0e560-abfa-4017-9cfc-263f18488eb4", "metadata": {}, "source": [ "複数のデータフレームを結合する操作は、データの統合や分析において非常に重要な手段です。Polarsでは、さまざまな結合方法が提供されており、データの構造に応じて最適な方法を選択することができます。以下では、Polarsで利用できる代表的な結合手法である`concat`、`join`、`join_asof`、`join_where`とデータフレームの結合メソッドについて紹介します。" ] }, { "cell_type": "code", "execution_count": 23, "id": "c2d4da51-0d23-47f3-a0b1-7270b4e8afa2", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import polars as pl\n", "from helper.jupyter import row" ] }, { "cell_type": "markdown", "id": "802062ef-ec51-4e7d-8496-8766896d7020", "metadata": {}, "source": [ "## concat" ] }, { "cell_type": "markdown", "id": "e753d479-1654-4927-a3cf-8a74ef143eeb", "metadata": {}, "source": [ "`pl.concat()`を使用すると、複数のデータフレームを縦または横に結合できます。結合方法は引数`how`で指定され、以下の5種類の結合方法があります。\n", "\n", "* `vertical`および`vertical_relaxed`: 縦方向の結合\n", "* `horizontal`: 横方向の結合\n", "* `diagonal`および`diagonal_relaxed`: 縦横両方向の結合\n", "* `align`: 縦横両方向の結合ですが、データをキーで整列してから結合を行います\n", "\n", "以下は、次の二つのデータフレームを使って、上記の結合方法について詳しく説明します。" ] }, { "cell_type": "code", "execution_count": 3, "id": "5c2ecb72-ca11-4f81-a451-960889179c1f", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (3, 2)
xy
i64i64
12
23
31
\n", "shape: (4, 2)
xy
i64i64
612
23
12
54
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df1 = pl.DataFrame({\"x\":[1, 2, 3], \"y\":[2, 3, 1]})\n", "df2 = pl.DataFrame({\"x\":[6, 2, 1, 5], \"y\":[12, 3, 2, 4]})\n", "row(df1, df2)" ] }, { "cell_type": "markdown", "id": "3cdeb222-9d83-4810-b81c-dcd6dd18eed4", "metadata": {}, "source": [ "### 縦結合" ] }, { "cell_type": "markdown", "id": "81afec5e-e027-4fd3-a490-dc51825ec049", "metadata": {}, "source": [ "以下のように、すべてのデータフレームの列名とデータ型が一致する場合は、`vertical`で縦に結合します。" ] }, { "cell_type": "code", "execution_count": 4, "id": "a4f8b0a9-6f2a-47f3-819d-9c143f6efba5", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (7, 2)
xy
i64i64
12
23
31
612
23
12
54
" ], "text/plain": [ "shape: (7, 2)\n", "┌─────┬─────┐\n", "│ x ┆ y │\n", "│ --- ┆ --- │\n", "│ i64 ┆ i64 │\n", "╞═════╪═════╡\n", "│ 1 ┆ 2 │\n", "│ 2 ┆ 3 │\n", "│ 3 ┆ 1 │\n", "│ 6 ┆ 12 │\n", "│ 2 ┆ 3 │\n", "│ 1 ┆ 2 │\n", "│ 5 ┆ 4 │\n", "└─────┴─────┘" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pl.concat([df1, df2], how='vertical')" ] }, { "cell_type": "markdown", "id": "d9867f03-4532-4375-b0ac-eb161f18654d", "metadata": {}, "source": [ "列名が一致するがデータ型が一致しない場合は、`vertical_relaxed`を使用して縦に結合します。この場合、結果のデータ型は上位のデータ型が採用されます。以下のコード例では、`df2`の`x`列を`Float64`型にキャストしてから結合しています。このように、`x`列のデータ型が`Float64`に統一され、縦に結合されます。" ] }, { "cell_type": "code", "execution_count": 5, "id": "39270b04-4119-403c-9323-292eb99bf6c3", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (7, 2)
xy
f64i64
1.02
2.03
3.01
6.012
2.03
1.02
5.04
" ], "text/plain": [ "shape: (7, 2)\n", "┌─────┬─────┐\n", "│ x ┆ y │\n", "│ --- ┆ --- │\n", "│ f64 ┆ i64 │\n", "╞═════╪═════╡\n", "│ 1.0 ┆ 2 │\n", "│ 2.0 ┆ 3 │\n", "│ 3.0 ┆ 1 │\n", "│ 6.0 ┆ 12 │\n", "│ 2.0 ┆ 3 │\n", "│ 1.0 ┆ 2 │\n", "│ 5.0 ┆ 4 │\n", "└─────┴─────┘" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pl.concat([\n", " df1, \n", " df2.with_columns(pl.col('x').cast(pl.Float64))\n", " ], \n", " how='vertical_relaxed')" ] }, { "cell_type": "markdown", "id": "159c15d6-c660-413a-a1b6-22fccd452d5b", "metadata": {}, "source": [ "Pandasのように縦結合するとき、各データフレームにキーを付ける方法についてのプログラム例を以下に示します。この方法では、`df1`と`df2`にそれぞれキーを付けてから縦に結合します。\n", "プログラムには、`key`列を追加して各データフレームの行にキーを付けてから縦に結合することで、元のデータフレームを識別できるようにしています。" ] }, { "cell_type": "code", "execution_count": 6, "id": "ab293f02-6f23-4740-8e20-d22f4d5c9755", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (7, 3)
keyxy
stri64i64
"A"12
"A"23
"A"31
"B"612
"B"23
"B"12
"B"54
" ], "text/plain": [ "shape: (7, 3)\n", "┌─────┬─────┬─────┐\n", "│ key ┆ x ┆ y │\n", "│ --- ┆ --- ┆ --- │\n", "│ str ┆ i64 ┆ i64 │\n", "╞═════╪═════╪═════╡\n", "│ A ┆ 1 ┆ 2 │\n", "│ A ┆ 2 ┆ 3 │\n", "│ A ┆ 3 ┆ 1 │\n", "│ B ┆ 6 ┆ 12 │\n", "│ B ┆ 2 ┆ 3 │\n", "│ B ┆ 1 ┆ 2 │\n", "│ B ┆ 5 ┆ 4 │\n", "└─────┴─────┴─────┘" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data = {\"A\":df1, \"B\":df2}\n", "pl.concat([\n", " df.select(pl.lit(key).alias(\"key\"), pl.all()) \n", " for key, df in data.items()\n", "])" ] }, { "cell_type": "markdown", "id": "0a1415f5-75ed-4381-aeda-d0c422edf720", "metadata": {}, "source": [ "### 横結合" ] }, { "cell_type": "markdown", "id": "f524f6a9-3b79-475f-981b-d231b1327020", "metadata": {}, "source": [ "列名が異なるデータフレームを横に結合するには、`horizontal`を使用します。以下のプログラムでは、`df1`と`df2`の列名を2種類の方法でリネームし、横結合します。このように、`df1`の列名に`1`を、`df2`の列名に`2`を付けて横に結合します。`df2`は`df1`より行数が多いため、`df1`に存在しない行には`null`が補完されます。" ] }, { "cell_type": "code", "execution_count": 7, "id": "3b7d517a-a272-4e1f-bca2-d8a76da1cb03", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 4)
x1y1x2y2
i64i64i64i64
12612
2323
3112
nullnull54
" ], "text/plain": [ "shape: (4, 4)\n", "┌──────┬──────┬─────┬─────┐\n", "│ x1 ┆ y1 ┆ x2 ┆ y2 │\n", "│ --- ┆ --- ┆ --- ┆ --- │\n", "│ i64 ┆ i64 ┆ i64 ┆ i64 │\n", "╞══════╪══════╪═════╪═════╡\n", "│ 1 ┆ 2 ┆ 6 ┆ 12 │\n", "│ 2 ┆ 3 ┆ 2 ┆ 3 │\n", "│ 3 ┆ 1 ┆ 1 ┆ 2 │\n", "│ null ┆ null ┆ 5 ┆ 4 │\n", "└──────┴──────┴─────┴─────┘" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pl.concat([\n", " df1.rename(lambda name:f\"{name}1\"), \n", " df2.select(pl.all().name.suffix(\"2\"))\n", "], how='horizontal')" ] }, { "cell_type": "markdown", "id": "83c88796-5c82-47dd-8203-1c054efe81d8", "metadata": {}, "source": [ "一部の列名が同じで、一部の列名が異なる場合、`diagonal`や`diagonal_relaxed`を使用して結合できます。`diagonal_relaxed`は自動的に上位のデータ型を採用します。次のプログラムでは、`df1`に`u`列が追加され、`df2`に`v`列が追加され、`diagonal`で二つのデータフレームを結合します。列名が一致するデータは縦に結合し、一致しない列は`NULL`で欠損値を表します。" ] }, { "cell_type": "markdown", "id": "ecdf3c1e-8c47-4bb6-8d38-ef3c09bbe6c2", "metadata": {}, "source": [ "### 縦と横結合" ] }, { "cell_type": "code", "execution_count": 8, "id": "4c78c54d-18fe-42f0-bf3c-c0604e1f0483", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (7, 4)
xyuv
i64i64i64i64
123null
235null
314null
612null72
23null6
12null2
54null20
" ], "text/plain": [ "shape: (7, 4)\n", "┌─────┬─────┬──────┬──────┐\n", "│ x ┆ y ┆ u ┆ v │\n", "│ --- ┆ --- ┆ --- ┆ --- │\n", "│ i64 ┆ i64 ┆ i64 ┆ i64 │\n", "╞═════╪═════╪══════╪══════╡\n", "│ 1 ┆ 2 ┆ 3 ┆ null │\n", "│ 2 ┆ 3 ┆ 5 ┆ null │\n", "│ 3 ┆ 1 ┆ 4 ┆ null │\n", "│ 6 ┆ 12 ┆ null ┆ 72 │\n", "│ 2 ┆ 3 ┆ null ┆ 6 │\n", "│ 1 ┆ 2 ┆ null ┆ 2 │\n", "│ 5 ┆ 4 ┆ null ┆ 20 │\n", "└─────┴─────┴──────┴──────┘" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dfs = [\n", " df1.with_columns(u=pl.col('x') + pl.col('y')),\n", " df2.with_columns(v=pl.col('x') * pl.col('y'))\n", "]\n", "pl.concat(dfs, how='diagonal')" ] }, { "cell_type": "markdown", "id": "f3c2a603-d129-4e5f-9e9e-16c2a5291719", "metadata": {}, "source": [ "### 整列結合" ] }, { "cell_type": "markdown", "id": "cc1c03f2-15c4-4e72-ab4b-b49ee1dccb9e", "metadata": {}, "source": [ "`align`結合は、`diagonal`と似ていますが、列名が一致するデータをキーとして集合化し、他の列の値を統合します。以下は、`df1`と`df2`に追加した列を使って`align`で結合する例です。このように、`align`結合では共通の`x`と`y`の値をキーとして行をマージし、他の列の値を統合しています。例えば、`x=1, y=2`の行は`u=3`と`v=2`が統合されて1行になります。" ] }, { "cell_type": "code", "execution_count": 9, "id": "0cafc3ad-5a6b-43ae-8e71-b50436fab760", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (5, 4)
xyuv
i64i64i64i64
1232
2356
314null
54null20
612null72
" ], "text/plain": [ "shape: (5, 4)\n", "┌─────┬─────┬──────┬──────┐\n", "│ x ┆ y ┆ u ┆ v │\n", "│ --- ┆ --- ┆ --- ┆ --- │\n", "│ i64 ┆ i64 ┆ i64 ┆ i64 │\n", "╞═════╪═════╪══════╪══════╡\n", "│ 1 ┆ 2 ┆ 3 ┆ 2 │\n", "│ 2 ┆ 3 ┆ 5 ┆ 6 │\n", "│ 3 ┆ 1 ┆ 4 ┆ null │\n", "│ 5 ┆ 4 ┆ null ┆ 20 │\n", "│ 6 ┆ 12 ┆ null ┆ 72 │\n", "└─────┴─────┴──────┴──────┘" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pl.concat(dfs, how=\"align\")" ] }, { "cell_type": "markdown", "id": "6f885c89-2eec-42a2-9a9e-d4f543749a04", "metadata": {}, "source": [ "`pl.align_frames()`を使用すると、複数のデータフレームを指定した列で整列させることができます。以下の例では、`df1`と`df2`を`x`および`y`列で整列させています。整列後の各データフレームの行数は同じで、指定された列の値に基づいて他の列が整列されています。" ] }, { "cell_type": "code", "execution_count": 10, "id": "3eae433d-dd50-4c57-80b5-0c49af81c8bd", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (5, 3)
xyu
i64i64i64
123
235
314
54null
612null
\n", "shape: (5, 3)
xyv
i64i64i64
122
236
31null
5420
61272
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(*pl.align_frames(*dfs, on=['x', 'y']))" ] }, { "cell_type": "markdown", "id": "e6a02173-afa4-45cf-bfc8-683c8dc59f50", "metadata": {}, "source": [ "## join" ] }, { "cell_type": "markdown", "id": "d3bc78b0-0112-4930-a3d8-f2ba6430fed9", "metadata": {}, "source": [ "`Polars`の`join()`メソッドは、SQLのように2つのデータフレームを結合するための方法を提供します。`join`は、異なる結合戦略を使用して、2つのデータフレームの対応する行をマッチさせることができます。\n", "\n", "```python\n", "df.join(\n", " other, # 結合するもう1つのDataFrame\n", " on=None, # 両方のDataFrameの結合に使う列名または式\n", " how='inner', # 結合方法(デフォルトは'inner')\n", " left_on=None, # 左側のDataFrameの結合列\n", " right_on=None, # 右側のDataFrameの結合列\n", " suffix='_right', # 重複した列名に付ける接尾辞\n", " validate='m:m', # 結合タイプの検証 ('m:m', 'm:1', '1:m', '1:1')\n", " join_nulls=False, # Null値もマッチさせるかどうか\n", " coalesce=None # 共通のキー列に対してnull値を埋めるかどうか\n", ")\n", "```\n", "\n", "引数`how`で結合方法を指定します。\n", "\n", "- **inner**: 両方のテーブルで一致する行を返す。\n", "- **left**: 左のテーブルのすべての行と、右のテーブルの一致する行を返す。\n", "- **right**: 右のテーブルのすべての行と、左のテーブルの一致する行を返す。\n", "- **full**: 左右どちらかに一致する行をすべて返す。\n", "- **semi**: 左テーブルから一致する行を返すが、右のテーブルからは列を返さない。\n", "- **anti**: 左テーブルの一致しない行を返す。" ] }, { "cell_type": "code", "execution_count": 11, "id": "02e6395e-ff2c-47c6-9fa3-dc3985d5e08e", "metadata": {}, "outputs": [], "source": [ "df_left = pl.DataFrame({\n", " \"id\": [1, 2, 3, 4],\n", " \"name\": [\"Alice\", \"Bob\", \"Charlie\", \"David\"]\n", "})\n", "\n", "df_right = pl.DataFrame({\n", " \"id\": [3, 4, 5],\n", " \"age\": [23, 30, 40]\n", "})" ] }, { "cell_type": "markdown", "id": "af92bd41-9a8b-47c6-b4f7-fd4375da63e8", "metadata": {}, "source": [ "### inner\n", "\n", "両方のデータフレームに存在する`id`に基づいて、内部結合を行います。次の例では、`id`が3と4に一致する行のみが返されました。" ] }, { "cell_type": "code", "execution_count": 12, "id": "57a07cba-5ad4-4128-ba60-15432664bd4d", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (2, 3)
idnameage
i64stri64
3"Charlie"23
4"David"30
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right, \n", " df_left.join(df_right, on=\"id\", how=\"inner\") \n", ")" ] }, { "cell_type": "markdown", "id": "df0ddeae-5c02-4061-a236-8e1fd00775ba", "metadata": {}, "source": [ "### leftとright\n", "\n", "左のデータフレームのすべての行を返し、右のデータフレームに一致するデータがあれば、それも含めます。次の例では、`id`が1と2の行は右に対応するデータがないため、`age`は`null`です。" ] }, { "cell_type": "code", "execution_count": 13, "id": "caa7b17e-73c9-446f-9ad0-6615f1dc8a00", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (4, 3)
idnameage
i64stri64
1"Alice"null
2"Bob"null
3"Charlie"23
4"David"30
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"left\")\n", ")" ] }, { "cell_type": "code", "execution_count": 14, "id": "d118ddb3-6071-4028-9d69-892f73064027", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (3, 3)
nameidage
stri64i64
"Charlie"323
"David"430
null540
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"right\")\n", ")" ] }, { "cell_type": "markdown", "id": "f7af0d6d-0804-4b48-bba3-722889e56248", "metadata": {}, "source": [ "### full\n", "\n", "両方のデータフレームのすべての行を返し、どちらかに存在するデータがあれば、それを含めます。左と右のどちらからデータを取得したかを区別するために、結果には二つの結合列が作成されます。右側の結合列には、重複を避けるために`_right`という接尾辞が追加されます。結果から、idが1と2の行は左側のデータにのみ存在し、idが5の行は右側のデータにのみ存在することがわかります。" ] }, { "cell_type": "code", "execution_count": 15, "id": "8fd7a8b0-c9bf-41a5-9286-79d91be0fd76", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (5, 4)
idnameid_rightage
i64stri64i64
1"Alice"nullnull
2"Bob"nullnull
3"Charlie"323
4"David"430
nullnull540
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"full\")\n", ")" ] }, { "cell_type": "markdown", "id": "9e3612fa-4f9d-4b46-88b8-15af5d7591be", "metadata": {}, "source": [ "`coalesce`引数を`True`に設定すると、これらの2つの列は1つにまとめられます。" ] }, { "cell_type": "code", "execution_count": 16, "id": "03b5626b-f82d-4812-9b6a-4feb9e038f26", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (5, 3)
idnameage
i64stri64
1"Alice"null
2"Bob"null
3"Charlie"23
4"David"30
5null40
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"full\", coalesce=True)\n", ")" ] }, { "cell_type": "markdown", "id": "f719e647-9b30-472d-932c-ea05ee100ffa", "metadata": {}, "source": [ "### semiとanti" ] }, { "cell_type": "markdown", "id": "66151d0f-5886-43ed-a3cd-7f1e9ce6a02d", "metadata": {}, "source": [ "`semi`は右側に存在する行を出力します。`anti`は右側に存在しない行を出力します。semiとantiの結果には、右側の列は含まれません。" ] }, { "cell_type": "code", "execution_count": 17, "id": "7e5f9a3d-ff20-4768-904c-184507235be3", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (2, 2)
idname
i64str
3"Charlie"
4"David"
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"semi\")\n", ")" ] }, { "cell_type": "code", "execution_count": 18, "id": "bf9d7204-f0ac-4c23-9fc6-dfbaa46bb005", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (2, 2)
idname
i64str
1"Alice"
2"Bob"
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, on=\"id\", how=\"anti\")\n", ")" ] }, { "cell_type": "markdown", "id": "6f052957-f213-4fea-939d-3a53cc395473", "metadata": {}, "source": [ "### cross" ] }, { "cell_type": "markdown", "id": "519a3f42-fddf-4969-9017-14cc3309541c", "metadata": {}, "source": [ "`cross`は、2つのデータフレームのデカルト積を出力します。つまり、左側のすべての行と右側のすべての行の組み合わせを結合します。この場合は、`on`引数の指定は不要になります。" ] }, { "cell_type": "code", "execution_count": 19, "id": "cdbf69d0-20d1-452e-b06c-a71e4cb5f41e", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
idname
i64str
1"Alice"
2"Bob"
3"Charlie"
4"David"
\n", "shape: (3, 2)
idage
i64i64
323
430
540
\n", "shape: (12, 4)
idnameid_rightage
i64stri64i64
1"Alice"323
1"Alice"430
1"Alice"540
2"Bob"323
2"Bob"430
3"Charlie"430
3"Charlie"540
4"David"323
4"David"430
4"David"540
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(\n", " df_left, df_right,\n", " df_left.join(df_right, how=\"cross\")\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "id": "b6e1a35f-a9ef-4d58-9712-5bf273b14eec", "metadata": {}, "source": [ "### validate\n", "\n", "`join` を使ってデータフレームを結合する際、結合キーに対応する左側と右側のデータ行数によって、次の 4 つのパターンが考えられます。 \n", "\n", "* **1:1(一対一)**: 左右の結合キーが **どちらもユニーク** である必要があります。(例: 社員 ID と 社員証番号) \n", "* **1:m(一対多)**: 左のキーは **ユニーク** だが、右のキーは **重複可**。(例: 部署と所属社員) \n", "* **m:1(多対一)**: 左のキーは **重複可** だが、右のキーは **ユニーク**。(例: 注文明細と商品マスタ) \n", "* **m:m(多対多)**: 両方のキーに **重複がある** ことを許容。(デフォルトの動作) \n", "\n", "マッチングする行が複数ある場合、デフォルトでは **マッチしたすべての組み合わせ(デカルト積)** が結果に含まれます。 \n", "\n", "`join()` の `validate` 引数を使用すると、指定したマッチング関係を制約できます。設定した関係に違反する場合はエラーが発生します。 \n", "\n", "次の 2 つのデータフレームには、`id == 3` の行がそれぞれ 2 つずつ含まれています。 " ] }, { "cell_type": "code", "execution_count": 20, "id": "31545199-4cf2-4593-9db9-a2194583c7c8", "metadata": {}, "outputs": [], "source": [ "df_left = pl.DataFrame({\n", " \"id\": [1, 2, 3, 4, 3],\n", " \"name\": [\"Alice\", \"Bob\", \"Charlie\", \"David\", \"charlie\"]\n", "})\n", "\n", "df_right = pl.DataFrame({\n", " \"id\": [3, 4, 5, 3],\n", " \"age\": [23, 30, 40, 46]\n", "})" ] }, { "cell_type": "markdown", "id": "e561e9c7-d71f-46b0-aec1-46ebc4911d50", "metadata": {}, "source": [ "このまま `id` をキーにして `join()` を実行すると、`id == 3` の行は **デカルト積** となり、結果には 4 行が生成されます。 " ] }, { "cell_type": "code", "execution_count": 21, "id": "8d09c1a9-85f1-43cb-a796-f4add93f0826", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (5, 3)
idnameage
i64stri64
3"Charlie"23
3"Charlie"46
4"David"30
3"charlie"23
3"charlie"46
" ], "text/plain": [ "shape: (5, 3)\n", "┌─────┬─────────┬─────┐\n", "│ id ┆ name ┆ age │\n", "│ --- ┆ --- ┆ --- │\n", "│ i64 ┆ str ┆ i64 │\n", "╞═════╪═════════╪═════╡\n", "│ 3 ┆ Charlie ┆ 23 │\n", "│ 3 ┆ Charlie ┆ 46 │\n", "│ 4 ┆ David ┆ 30 │\n", "│ 3 ┆ charlie ┆ 23 │\n", "│ 3 ┆ charlie ┆ 46 │\n", "└─────┴─────────┴─────┘" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_left.join(df_right, on=\"id\")" ] }, { "cell_type": "markdown", "id": "67306ac0-1944-40d3-b981-ec16639b56e8", "metadata": {}, "source": [ "`validate=\"1:1\"` を指定すると、結合キーに対して **一対一の関係** が満たされない場合にエラーとなり、データの整合性をチェックできます。 " ] }, { "cell_type": "code", "execution_count": 25, "id": "3327214a-8acd-4be1-b4b5-dd962d02098d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ComputeError: join keys did not fulfill 1:1 validation\n" ] } ], "source": [ "%%capture_except\n", "df_left.join(df_right, on=\"id\", validate='1:1')" ] }, { "cell_type": "markdown", "id": "31ad8a2a-ebb0-4613-92db-43bf911c99b6", "metadata": {}, "source": [ "## update\n", "\n", "`DataFrame.update`メソッドは、Polarsにおいてインデックス列を基に既存のデータフレームを更新するために使用されます。引数`on`で指定された列が一致する行に対して、他の列のデータを更新します。\n", "\n", "以下のコードは、`df`と`df_update`の`id`列が一致する行を基に、`df_update`の他の列(`x`と`y`)の値を`df`に更新する例です。" ] }, { "cell_type": "code", "execution_count": 21, "id": "6fb9ae91-3d25-4cec-9c08-2f9212c2826c", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 3)
idxy
i64i64i64
11040
22030
33020
44010
\n", "shape: (2, 3)
idxy
i64i64i64
1100-10
3300-20
\n", "shape: (4, 3)
idxy
i64i64i64
1100-10
22030
3300-20
44010
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df = pl.DataFrame({\n", " \"id\": [1, 2, 3, 4],\n", " \"x\": [10, 20, 30, 40],\n", " \"y\": [40, 30, 20, 10]\n", "})\n", "\n", "df_update = pl.DataFrame({\n", " \"id\": [1, 3],\n", " \"x\": [100, 300],\n", " \"y\": [-10, -20],\n", "})\n", "\n", "df_res = df.update(df_update, on='id')\n", "row(df, df_update, df_res)" ] }, { "cell_type": "markdown", "id": "597a4e43-1a0d-45ca-8d77-36ce223e63d9", "metadata": {}, "source": [ "## join_asof" ] }, { "cell_type": "markdown", "id": "a9b3a06a-41bc-45dc-8132-7a388c4be83f", "metadata": {}, "source": [ "`join_asof`は、時間や数値のような連続的なデータに基づいて2つのDataFrameを「概ね一致」させて結合するメソッドです。これは、正確な一致ではなく、片方の値がもう片方の値の近くにある場合に使われます。主に、**時系列データ**のような順序のあるデータで利用されます。\n", "\n", "`join_asof`は通常、次のような状況で使われます:\n", "- 片方のデータが特定の時間に対するスナップショットを持ち、もう片方がその時間に最も近い値を持っている場合。\n", "- \"前方一致\"または\"後方一致\"など、指定された方向に最も近いデータを探す場合。" ] }, { "cell_type": "code", "execution_count": 22, "id": "ce6f848c-21a1-4ed9-a595-22791466de67", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (5, 2)
timeevent
i64str
1"A"
5"B"
10"C"
15"D"
20"E"
\n", "shape: (4, 2)
timeprice
i64i64
2100
6105
12110
18115
\n", "shape: (5, 3)
timeeventprice
i64stri64
1"A"null
5"B"100
10"C"105
15"D"110
20"E"115
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import polars as pl\n", "\n", "df1 = pl.DataFrame(\n", " {\n", " \"time\": [1, 5, 10, 15, 20],\n", " \"event\": [\"A\", \"B\", \"C\", \"D\", \"E\"],\n", " }\n", ")\n", "\n", "df2 = pl.DataFrame(\n", " {\n", " \"time\": [2, 6, 12, 18],\n", " \"price\": [100, 105, 110, 115],\n", " }\n", ")\n", "\n", "result = df1.join_asof(df2, on=\"time\", strategy=\"backward\")\n", "row(df1, df2, result)" ] }, { "cell_type": "markdown", "id": "b320bf0e-4fe8-4f28-9c4a-286799b2a96e", "metadata": {}, "source": [ "`df1`の各行に対して、`df2`の`\"time\"`列で最も近くて「過去または現在の時間」にあたる行を結合します。つまり、`df1`の各行に対して、`df2`でその`\"time\"`に一番近い過去の`\"price\"`の値を結合します。`strategy=\"backward\"`は、`df1`の`\"time\"`の値に対して、それよりも過去または同時刻の`df2`の値を選ぶという戦略です。もう一つのオプションに`\"forward\"`があり、これは未来の値を選択します。" ] }, { "cell_type": "markdown", "id": "3de94b61-53c7-4d2f-8060-e485cfb2c312", "metadata": {}, "source": [ "## join_where" ] }, { "cell_type": "markdown", "id": "693bd26b-88c5-40f0-9b81-f1153fd7421f", "metadata": {}, "source": [ "`join_where`では二つのDataFrameの列同士の比較条件を指定して、それに基づいて結合を行います。例えば、以下のコードでは、`df1`のtime列の値が、`df2`のtime_span列に含まれる時間範囲内にある場合に結合が行われます。複数の条件式がある場合は、それらすべての条件式を満たす場合にのみ結合が行われます。" ] }, { "cell_type": "code", "execution_count": 23, "id": "f9b8d5b0-c2f7-4f69-8707-d6b4e9c1f4f5", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (3, 2)
idtime
i64i64
100120
101140
102160
\n", "shape: (4, 2)
t_idtime_span
i64list[i64]
404[100, 110]
498[110, 130]
676[90, 100]
742[150, 170]
\n", "shape: (2, 4)
idtimet_idtime_span
i64i64i64list[i64]
100120498[110, 130]
102160742[150, 170]
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df1 = pl.DataFrame(\n", " {\n", " \"id\": [100, 101, 102],\n", " \"time\": [120, 140, 160],\n", " }\n", ")\n", "df2 = pl.DataFrame(\n", " {\n", " \"t_id\": [404, 498, 676, 742],\n", " \"time_span\": [(100, 110), (110, 130), (90, 100), (150, 170)],\n", " }\n", ")\n", "df_res = df1.join_where(\n", " df2,\n", " pl.col('time') >= pl.col('time_span').list.get(0),\n", " pl.col('time') <= pl.col('time_span').list.get(1)\n", ")\n", "row(df1, df2, df_res)" ] }, { "cell_type": "markdown", "id": "4ced7fae-c536-4d9c-97d5-4251ef6720f3", "metadata": {}, "source": [ "`.join_where()`はブール演算や`is_between()`もサポートしています。" ] }, { "cell_type": "code", "execution_count": 24, "id": "3993378b-e4a0-416d-b51b-1da3f4495762", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (2, 4)
idtimet_idtime_span
i64i64i64list[i64]
100120498[110, 130]
102160742[150, 170]
\n", "shape: (2, 4)
idtimet_idtime_span
i64i64i64list[i64]
100120498[110, 130]
102160742[150, 170]
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df_res1 = df1.join_where(\n", " df2,\n", " (pl.col('time') >= pl.col('time_span').list.get(0)) & (pl.col('time') <= pl.col('time_span').list.get(1))\n", ")\n", "\n", "df_res2 = df1.join_where(\n", " df2,\n", " pl.col('time').is_between(pl.col('time_span').list.get(0), pl.col('time_span').list.get(1))\n", ")\n", "row(df_res1, df_res2)" ] }, { "cell_type": "markdown", "id": "1950c6c0-0089-4277-85d4-0fd26f012518", "metadata": {}, "source": [ "`.join_where()`内部では、二つのデータフレームの行のデカルト積を計算し、その中から条件を満たす行だけをフィルタリングして出力します。そのため、データの件数が多い場合、処理が非常に遅くなる可能性があります。例えば、次のコードでは、二つの点群間で距離が0.01より小さい点のペアを計算します。この処理は計算量が $N^2$ に比例するため、大きなデータセットでは速度が非常に遅くなります。" ] }, { "cell_type": "code", "execution_count": 87, "id": "8356e387-7e02-4c21-93ce-3f2e0844e139", "metadata": {}, "outputs": [], "source": [ "n = 4000\n", "np.random.seed(42)\n", "df1 = pl.DataFrame(dict(\n", " x1=np.random.normal(size=n),\n", " y1=np.random.normal(size=n),\n", "))\n", "df2 = pl.DataFrame(dict(\n", " x2=np.random.normal(size=n),\n", " y2=np.random.normal(size=n),\n", "))" ] }, { "cell_type": "code", "execution_count": 88, "id": "99a510fa-4b1c-4695-88f8-15e738c41bf6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: total: 891 ms\n", "Wall time: 435 ms\n" ] } ], "source": [ "%%time\n", "df_res1 = (\n", " df1\n", " .join_where(\n", " df2, \n", " (pl.col('x2') - pl.col('x1'))**2 + (pl.col('y2') - pl.col('y1'))**2 < 0.0001\n", " )\n", " .sort('x1', 'y1')\n", ")" ] }, { "cell_type": "markdown", "id": "340a5664-68e4-4d7d-a7cc-e68da0cdb0b3", "metadata": {}, "source": [ "SciPyの`KDTree`を使用することで、このような処理を高速化できます。`KDTree`を利用すると、空間的な距離を効率的に計算し、特定の距離内にある点のペアを高速に見つけることができます。以下のコードでは、`tree1.query_ball_tree(tree2, 0.01)`を使用して範囲内の点のペアを計算し、結果を基に`df_pair`を作成します。その後、行番号をキーとして`df1`、`df_pair`、`df2`を結合して結果データフレーム`df_res2`を作成します。" ] }, { "cell_type": "code", "execution_count": 89, "id": "7272eb38-a937-42ba-83e4-e0f3cd69c3a0", "metadata": {}, "outputs": [], "source": [ "from scipy.spatial import KDTree" ] }, { "cell_type": "code", "execution_count": 101, "id": "15e0e248-d1b6-4c2e-aa62-65de32f86ace", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: total: 31.2 ms\n", "Wall time: 19 ms\n" ] } ], "source": [ "%%time\n", "tree1 = KDTree(df1.to_numpy())\n", "tree2 = KDTree(df2.to_numpy())\n", "res = tree1.query_ball_tree(tree2, 0.01)\n", "\n", "df_pair = (\n", " pl.DataFrame(pl.Series('index2', res, dtype=pl.List(pl.UInt32))) # resをデータフレームに変換\n", " .with_row_index('index1')\n", " .filter(pl.col('index2').list.len() > 0) # 範囲内の点が存在する行のみを残す\n", " .explode('index2') # リストを展開して1行1ペアにする\n", ")\n", "df_res2 = (\n", " df1\n", " .with_row_index('index1')\n", " .join(df_pair, on='index1')\n", " .join(\n", " df2.with_row_index('index2'), on='index2')\n", " .drop('index1', 'index2')\n", " .sort('x1', 'y1')\n", ")" ] }, { "cell_type": "code", "execution_count": 94, "id": "7dc153cc-bd03-41df-9427-63fa27913fa0", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (420, 4)
x1y1x2y2
f64f64f64f64
-2.065083-0.70744-2.05916-0.70436
-1.7690760.817306-1.7684150.825853
-1.523187-0.501234-1.514414-0.501647
-1.519346-0.035096-1.527461-0.03116
-1.4785220.932435-1.4823970.94037
1.669070.3888191.6785740.385821
1.696456-0.0387431.692313-0.037887
1.7349370.1429911.7379850.135518
1.831459-0.5327251.831177-0.523104
1.9541570.5682731.9489630.563404
\n", "shape: (420, 2)
index1index2
u32u32
101832
102789
411502
44739
452018
39311196
39351035
39641214
3978715
39941588
\n", "shape: (420, 4)
x1y1x2y2
f64f64f64f64
-2.065083-0.70744-2.05916-0.70436
-1.7690760.817306-1.7684150.825853
-1.523187-0.501234-1.514414-0.501647
-1.519346-0.035096-1.527461-0.03116
-1.4785220.932435-1.4823970.94037
1.669070.3888191.6785740.385821
1.696456-0.0387431.692313-0.037887
1.7349370.1429911.7379850.135518
1.831459-0.5327251.831177-0.523104
1.9541570.5682731.9489630.563404
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "row(df_res1, df_pair, df_res2)" ] }, { "cell_type": "markdown", "id": "aaba9a07-b00b-4696-9f72-2b0a70651f9f", "metadata": {}, "source": [ "`res`は二重リストで構成されており、各要素は`tree1`内の各点に対応しています。それぞれの要素には、対応する`tree2`内で指定範囲内にある点のインデックス(番号)のリストが格納されています。次のコードは、`tree1`の先頭50点について計算結果を表示し、空リスト(範囲内に点がない場合)をスキップします。\n", "\n", "この出力から、`tree1`の点10は、`tree2`の点1832と2789が距離0.01以内に存在することが分かります。" ] }, { "cell_type": "code", "execution_count": 102, "id": "a64aa210-515b-493f-b8d5-0e5bfdb64a4f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10 [1832, 2789]\n", "41 [1502]\n", "44 [739]\n", "45 [2018]\n" ] } ], "source": [ "for i, index2 in enumerate(res[:50]):\n", " if index2:\n", " print(i, index2)" ] }, { "cell_type": "markdown", "id": "4176b902-fb0d-44eb-a194-edb231a9b92a", "metadata": {}, "source": [ "## extendとvstackメソッド" ] }, { "cell_type": "markdown", "id": "2fb4a09d-8c03-4a0a-9f47-3089ee98242b", "metadata": {}, "source": [ "`DataFrame.extend()` と `DataFrame.vstack()` は、既存のデータフレームに別のデータフレームの内容を行として追加するメソッドです。\n", "\n", "- **`extend()`**: 元のデータフレームを直接変更します(その場で変更)。変更後のデータフレームは返り値として返します。\n", "- **`vstack()`**: `in_place` 引数を指定することで、元のデータフレームを直接変更する(その場で変更)か、新しいデータフレームを返すかを選べます。" ] }, { "cell_type": "code", "execution_count": 14, "id": "a4aa28d7-4159-4eb9-8472-33f34d8fa035", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (4, 2)
ab
i64i64
13
24
57
68
\n", "shape: (4, 2)
ab
i64i64
13
24
57
68
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df1 = pl.DataFrame({\"a\": [1, 2], \"b\": [3, 4]})\n", "df2 = pl.DataFrame({\"a\": [5, 6], \"b\": [7, 8]})\n", "\n", "# clone()を使って元のデータフレームを複製\n", "df_extend = df1.clone()\n", "df_extend.extend(df2) # extendで直接変更\n", "\n", "df_vstack = df1.clone()\n", "df_vstack.vstack(df2, in_place=True) # vstackでその場で変更\n", "row(df_extend, df_vstack)" ] }, { "cell_type": "markdown", "id": "3b52fa63-dbaf-413a-aa49-d27e9bcbf0cb", "metadata": {}, "source": [ "両者の違いは、メモリ管理の方法にあります。\n", "\n", "`extend()` は、引数のデータフレームの内容を元のデータフレームのメモリの末尾にコピーします。結果として、結合後のデータフレームのメモリは連続しています。ただし、元のデータフレームのメモリが再配置される可能性があるため、ループ内で頻繁に `extend()` を使用することは推奨されません。\n", "\n", "`vstack()` は、結合する2つのデータフレームのメモリをそのままにし、リンクによって両者を結合します。そのため、結果として得られるデータフレームのメモリは不連続になります。この不連続性により、後で計算を行う際のパフォーマンスが低下する可能性があります。 \n", "一般的には、ループ内で `vstack()` を使用してデータを結合し、最後に `rechunk()` を実行してメモリを連続した状態に変換するのが効率的です。" ] }, { "cell_type": "code", "execution_count": 15, "id": "90502bf1-1e75-45ac-8901-e138cc8410df", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "df_extend.n_chunks() = 1\n", "df_vstack.n_chunks() = 2\n", "df_vstack.rechunk().n_chunks() = 1\n" ] } ], "source": [ "print(f'{df_extend.n_chunks() = }') # extend後のチャンク数\n", "print(f'{df_vstack.n_chunks() = }') # vstack後のチャンク数\n", "print(f'{df_vstack.rechunk().n_chunks() = }') # rechunk後のチャンク数" ] }, { "cell_type": "markdown", "id": "f2629a50-d836-47cd-acfa-0cf9ed1f8b79", "metadata": {}, "source": [ "## hstackメソッド" ] }, { "cell_type": "markdown", "id": "70a299ff-d4bd-426f-9ff8-fa15c25a3f33", "metadata": {}, "source": [ "`DataFrame.hstack()` は、2つのデータフレームを水平方向(列方向)に結合します。`in_place=True` を指定すると、元のデータフレームに列を追加します。" ] }, { "cell_type": "code", "execution_count": 18, "id": "b8b59f9c-5140-475e-af33-b8e4fcc63b49", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (2, 4)
abcd
i64i64i64i64
1357
2468
\n", "shape: (2, 4)
abcd
i64i64i64i64
1357
2468
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df1 = pl.DataFrame({\"a\": [1, 2], \"b\": [3, 4]})\n", "df2 = pl.DataFrame({\"c\": [5, 6], \"d\": [7, 8]})\n", "\n", "# in-place操作なし(新しいデータフレームを返す)\n", "df3 = df1.hstack(df2)\n", "\n", "# in-place操作あり\n", "df1.hstack(df2, in_place=True)\n", "row(df3, df1)" ] }, { "cell_type": "markdown", "id": "ae632a19-f9b4-45f5-b161-4ab9e5995e4b", "metadata": {}, "source": [ "## merge_sortedメソッド" ] }, { "cell_type": "markdown", "id": "9e4a7d62-23ac-4d5d-9217-37d8f7e209a7", "metadata": {}, "source": [ "`DataFrame.merge_sorted()` は、**あらかじめソートされた** 2つのデータフレームを縦方向に結合し、指定したキー列に基づいてソートされた新しいデータフレームを返すメソッドです。このメソッドを使用する際は、入力となる両方のデータフレームが事前に指定したキー列でソートされていることが前提となります。" ] }, { "cell_type": "code", "execution_count": 26, "id": "03594f09-9199-448b-b418-69c7b79fd117", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "shape: (6, 2)
ab
i64i64
14
25
36
43
52
67
\n", "shape: (6, 2)
ab
i64i64
52
43
14
25
36
67
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df1 = pl.DataFrame({\"a\": [3, 1, 5], \"b\": [6, 4, 2]})\n", "df2 = pl.DataFrame({\"a\": [6, 2, 4], \"b\": [7, 5, 3]})\n", "\n", "# \"a\" 列でソートしてからマージ\n", "df3 = df1.sort(\"a\").merge_sorted(df2.sort(\"a\"), key=\"a\")\n", "\n", "# \"b\" 列でソートしてからマージ\n", "df4 = df1.sort(\"b\").merge_sorted(df2.sort(\"b\"), key=\"b\")\n", "\n", "row(df3, df4)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 5 }