连接
join
将一个或多个DataFrame中的列组合成一个新的DataFrame, 不同类型的连接使用不同的**"连接策略"**和匹配条件会影响列的组合方式以及连接结果中包含的行
最常见的连接类型是"等值连接". Polars支持多种等值连接策略, 这些策略决定了如何处理行匹配. Polars还支持"非等值连接", 即匹配条件为不想等的连接类型,
以及一种通过键的接近性匹配行的连接类型, 成为"asof连接"
等值连接
等值连接中, 通过检查表达式的相等性来匹配行, 话不多说, 看代码简单易懂
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham")
16
17print(new_df)
上面代码的第15行中的on
参数指定了根据哪一列来进行匹配连接, 这里选择了ham
列, 因为df
和other_df
的ham
列都有"a"
和"b"
, 所以最后结果有两行
1shape: (2, 4)
2┌─────┬─────┬─────┬───────┐
3│ foo ┆ bar ┆ ham ┆ apple │
4│ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str │
6╞═════╪═════╪═════╪═══════╡
7│ 1 ┆ 6.0 ┆ a ┆ x │
8│ 2 ┆ 7.0 ┆ b ┆ y │
9└─────┴─────┴─────┴───────┘
动态计算连接的键
如果我们想进行连接的两个列的列名不同, 然后其中一列还想做些处理再进行匹配, 可以看下面代码
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham_other": ["A", "B", "D"]
13 }
14)
15new_df = df.join(other_df,
16 left_on="ham",
17 right_on=pl.col("ham_other").str.to_lowercase()
18 )
19
20print(new_df)
我们指定left_on
和right_on
参数, left_on
指定了df
的列名, right_on
指定了other_df
的列名,
pl.col("ham_other").str.to_lowercase()
将other_df
的ham_other
列转换为小写然后再进行匹配
1shape: (2, 5)
2┌─────┬─────┬─────┬───────┬───────────┐
3│ foo ┆ bar ┆ ham ┆ apple ┆ ham_other │
4│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str ┆ str │
6╞═════╪═════╪═════╪═══════╪═══════════╡
7│ 1 ┆ 6.0 ┆ a ┆ x ┆ A │
8│ 2 ┆ 7.0 ┆ b ┆ y ┆ B │
9└─────┴─────┴─────┴───────┴───────────┘
连接策略
内连接
这是默认策略, 生成的DataFrame仅来自左右两个DataFrame中匹配的行, 我们还用第一个例子来展示, 结果只包含ham
相等的.
结果不包含df
的c
这一行和other_df
的d
这一行
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="inner")
16
17print(new_df)
1shape: (2, 4)
2┌─────┬─────┬─────┬───────┐
3│ foo ┆ bar ┆ ham ┆ apple │
4│ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str │
6╞═════╪═════╪═════╪═══════╡
7│ 1 ┆ 6.0 ┆ a ┆ x │
8│ 2 ┆ 7.0 ┆ b ┆ y │
9└─────┴─────┴─────┴───────┘
左连接
左连接生成的结果包含左边侧的所有行, 右侧匹配的行, 右侧没有的会使用null
替代, 我们来看代码和结果
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="left")
16
17print(new_df)
1shape: (3, 4)
2┌─────┬─────┬─────┬───────┐
3│ foo ┆ bar ┆ ham ┆ apple │
4│ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str │
6╞═════╪═════╪═════╪═══════╡
7│ 1 ┆ 6.0 ┆ a ┆ x │
8│ 2 ┆ 7.0 ┆ b ┆ y │
9│ 3 ┆ 8.0 ┆ c ┆ null │
10└─────┴─────┴─────┴───────┘
右连接
右连接和左连接相反, 不再赘述, 直接看代码
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="right")
16
17print(new_df)
1shape: (3, 4)
2┌──────┬──────┬───────┬─────┐
3│ foo ┆ bar ┆ apple ┆ ham │
4│ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str │
6╞══════╪══════╪═══════╪═════╡
7│ 1 ┆ 6.0 ┆ x ┆ a │
8│ 2 ┆ 7.0 ┆ y ┆ b │
9│ null ┆ null ┆ z ┆ d │
10└──────┴──────┴───────┴─────┘
完全连接
完全连接将保留左侧和右侧数据框中的所有行, 即使在另一个DataFrame中没有匹配到的行
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="full")
16
17print(new_df)
1shape: (4, 5)
2┌──────┬──────┬──────┬───────┬───────────┐
3│ foo ┆ bar ┆ ham ┆ apple ┆ ham_right │
4│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str ┆ str │
6╞══════╪══════╪══════╪═══════╪═══════════╡
7│ 1 ┆ 6.0 ┆ a ┆ x ┆ a │
8│ 2 ┆ 7.0 ┆ b ┆ y ┆ b │
9│ null ┆ null ┆ null ┆ z ┆ d │
10│ 3 ┆ 8.0 ┆ c ┆ null ┆ null │
11└──────┴──────┴──────┴───────┴───────────┘
如果我们想把上面的ham
和ham_right
合并为一列, 可以指定参数coalesce=True
, 来看代码
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="full",coalesce=True)
16
17print(new_df)
1shape: (4, 4)
2┌──────┬──────┬─────┬───────┐
3│ foo ┆ bar ┆ ham ┆ apple │
4│ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str ┆ str │
6╞══════╪══════╪═════╪═══════╡
7│ 1 ┆ 6.0 ┆ a ┆ x │
8│ 2 ┆ 7.0 ┆ b ┆ y │
9│ null ┆ null ┆ d ┆ z │
10│ 3 ┆ 8.0 ┆ c ┆ null │
11└──────┴──────┴─────┴───────┘
半连接
半连接和左连接类似, 但是只返回左侧数据框中的行, 忽略右侧数据框中的行
半连接充当基于第二个数据框的一种行过滤器
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="semi")
16
17print(new_df)
1shape: (2, 3)
2┌─────┬─────┬─────┐
3│ foo ┆ bar ┆ ham │
4│ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str │
6╞═════╪═════╪═════╡
7│ 1 ┆ 6.0 ┆ a │
8│ 2 ┆ 7.0 ┆ b │
9└─────┴─────┴─────┘
反连接
返回左侧数据中与右侧数据不匹配的行
1import polars as pl
2df = pl.DataFrame(
3 {
4 "foo": [1, 2, 3],
5 "bar": [6.0, 7.0, 8.0],
6 "ham": ["a", "b", "c"]
7 }
8)
9other_df = pl.DataFrame(
10 {
11 "apple": ["x", "y", "z"],
12 "ham": ["a", "b", "d"]
13 }
14)
15new_df = df.join(other_df, on="ham", how="anti")
16
17print(new_df)
1shape: (1, 3)
2┌─────┬─────┬─────┐
3│ foo ┆ bar ┆ ham │
4│ --- ┆ --- ┆ --- │
5│ i64 ┆ f64 ┆ str │
6╞═════╪═════╪═════╡
7│ 3 ┆ 8.0 ┆ c │
8└─────┴─────┴─────┘
非等值连接
在非等值连接中, 左右数据框之间的匹配计算方式不同. 我们通过一个谓词来确定如何匹配行
使用场景
- 范围匹配
- 时间段匹配
- 薪资等级划分
- 区间重叠检测
- 排名和比较
下面代码中高亮的行是用来匹配的条件, 后面还可以写其他条件, 但是这里我们只用一个条件来演示.
1import polars as pl
2east = pl.DataFrame(
3 {
4 "id": [100, 101, 102],
5 "dur": [120, 140, 160],
6 "rev": [12, 14, 16],
7 "cores": [2, 8, 4],
8 }
9)
10west = pl.DataFrame(
11 {
12 "t_id": [404, 498, 676, 742],
13 "time": [90, 130, 150, 170],
14 "cost": [9, 13, 15, 16],
15 "cores": [4, 2, 1, 4],
16 }
17)
18res = east.join_where(
19 west,
20 pl.col("dur") < pl.col("time")
21)
22print(res)
观察下面的结果, 左侧条件dur
的值是120,140,160, 右侧time
值是90,130,150,170, 然后我们看匹配过程
- 左侧的120: 右侧的130,150,170
- 左侧的140: 右侧的150,170
- 左侧的160: 右侧的170
所以一共有6行, 整个连接过程类似于笛卡尔积, 然后再过滤
1shape: (6, 8)
2┌─────┬─────┬─────┬───────┬──────┬──────┬──────┬─────────────┐
3│ id ┆ dur ┆ rev ┆ cores ┆ t_id ┆ time ┆ cost ┆ cores_right │
4│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
5│ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
6╞═════╪═════╪═════╪═══════╪══════╪══════╪══════╪═════════════╡
7│ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 498 ┆ 130 ┆ 13 ┆ 2 │
8│ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
9│ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
10│ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
11│ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
12│ 102 ┆ 160 ┆ 16 ┆ 4 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
13└─────┴─────┴─────┴───────┴──────┴──────┴──────┴─────────────┘
NOTE
处于试验阶段, 还不稳定, 尚不支持任意布尔表达式作为谓词.
Asof连接
是一种特殊的非等值连接, 在时间序列数据分析中尤为有用.
它的核心思想是:对于左表中的每一行, 用右表中的指定键匹配,并且时间戳(或其他有序键)最接近(通常是小于或等于)左表时间戳的行
类似于左连接, 只不过我们匹配的是最接近的键, 而不是相等的, 在Polars中使用join_asof
特点:
- 基于有序键(通常是时间戳)
- 不精确匹配
- 单向性
- 通常用于回填或关联历史数据
on
指定的字段是用于匹配的键, 两个DataFrame都必须按on
键排序.
strategy
指定匹配策略
- backward: 搜索选择右侧DataFrame中 'on' 键小于或等于左侧键的最后一行
- forward: 搜索选择右侧DataFrame 中'on' 键大于或等于左侧键的第一行
- nearest: 搜索选择右侧DataFrame中值最接近左侧键的最后一行, 最近搜索当前不支持字符串键。
1import polars as pl
2from datetime import date
3population = pl.DataFrame(
4 {
5 "date": [date(2016, 3, 1), date(2018, 8, 1), date(2019, 1, 1)],
6 "population": [82.19, 82.66, 83.12],
7 }
8).sort("date")
9
10gdp = pl.DataFrame(
11 {
12 "date": [
13
14 date(2016, 1, 1),
15 date(2017, 1, 1),
16 date(2018, 1, 1),
17 date(2019, 1, 1),
18 date(2020, 1, 1),
19 ],
20 "gdp": [4164, 4411, 4566, 4696, 4827],
21 }
22)
23res = population.join_asof(gdp, on="date", strategy="backward")
24print(res)
我们来分析匹配过程: 针对左侧的三个时间点, 匹配过程如下: 针对左侧表的每一行数据, 都在右侧数据中找到最接近的值
- 2016-03-01: 右侧小于它的只有一个值, 即
2016-01-01
, gdp的值是4164
- 2018-08-01: 右侧小于它的有三个值, 即
2016-01-01
,2017-01-01
和2018-01-01
, 最近接的是2018-01-01
, gdp的值是4566
- 2019-01-01: 右侧小于或等于它的有四个值, 即
2016-01-01
,2017-01-01
,2018-01-01
和2019-01-01
, 最近接的是2019-01-01
, gdp的值是4696
1shape: (3, 3)
2┌────────────┬────────────┬──────┐
3│ date ┆ population ┆ gdp │
4│ --- ┆ --- ┆ --- │
5│ date ┆ f64 ┆ i64 │
6╞════════════╪════════════╪══════╡
7│ 2016-03-01 ┆ 82.19 ┆ 4164 │
8│ 2018-08-01 ┆ 82.66 ┆ 4566 │
9│ 2019-01-01 ┆ 83.12 ┆ 4696 │
10└────────────┴────────────┴──────┘
下面是一个更简短的例子, 我们使用strategy="forward"
策略
1df1 = pl.DataFrame({
2 "a":[1,2,3]
3})
4df2 = pl.DataFrame({
5 "a":[1,3,4],
6 "b":["x","y","z"],
7})
8res = df1.join_asof(df2,on=pl.col("a"), strategy="forward")
9print(res)
我们来写下匹配过程
- 1 -> (1,3,4) , 最匹配的是1, 值是x
- 3 -> (3,4) , 最匹配的是3, 值是y
- 4 -> (3) , 最匹配的是5, 值是y
1shape: (3, 2)
2┌─────┬─────┐
3│ a ┆ b │
4│ --- ┆ --- │
5│ i64 ┆ str │
6╞═════╪═════╡
7│ 1 ┆ x │
8│ 2 ┆ y │
9│ 3 ┆ y │
10└─────┴─────┘
笛卡尔积
只需要指定how="cross"
即可
1import polars as pl
2df1 = pl.DataFrame({
3 "a":[1,2],
4 "b":["x1",'x2']
5})
6df2 = pl.DataFrame({
7 "c":["apple","peach","pear"]
8})
9res = df1.join(df2,how="cross")
10print(res)
1shape: (6, 3)
2┌─────┬─────┬───────┐
3│ a ┆ b ┆ c │
4│ --- ┆ --- ┆ --- │
5│ i64 ┆ str ┆ str │
6╞═════╪═════╪═══════╡
7│ 1 ┆ x1 ┆ apple │
8│ 1 ┆ x1 ┆ peach │
9│ 1 ┆ x1 ┆ pear │
10│ 2 ┆ x2 ┆ apple │
11│ 2 ┆ x2 ┆ peach │
12│ 2 ┆ x2 ┆ pear │
13└─────┴─────┴───────┘