数据STUDIO

Pandas 的 join 和 merge 到底哪个更快?


数据STUDIO转载|作者:小小明
上 次云朵君在不严谨的测试下,得出了 join 可以比 merge 快5倍的结论。虽然默认参数用法下, join 确实比 merge 快一些,但实际上 join 并不见得会比 merge 快。链接:《 再见!不再使用 Pandas 中的 Merge 方法 》

下面是 数据处理专家——小小明 对上述结论进行纠偏。 继续深入测试,首先构造如下生成测试数据的方法:

import pandas as pd
import numpy as np
from time import time

def create_df(col_prefix, nrows=1000_0000):
    data = np.random.randint(0, 255, size=(nrows, 4))
    df = pd.DataFrame(data, columns=map(f"{col_prefix}_{{}}".format, range(4)), dtype="uint8")
    df["idx"] = df.index.values
    return df

生成的测试数据格式如下:

print(create_df("c"))
         c_0  c_1  c_2  c_3      idx
0 111 32 70 73 0
1 12 78 38 205 1
2 9 52 203 171 2
3 200 22 99 19 3
4 124 205 233 65 4
... ... ... ... ... ...
9999995 41 136 246 162 9999995
9999996 209 16 73 51 9999996
9999997 132 41 46 234 9999997
9999998 163 101 120 184 9999998
9999999 116 189 205 40 9999999

[10000000 rows x 5 columns]

默认生成1千万条数据,连接字段idx确保唯一。

初步测试

我们按照原文的逻辑简单测试一下:

df1 = create_df("x")
df2 = create_df("y")

start = time()
df1.merge(df2, on="idx")
print(f"merge耗时:{time() - start:.2f}s")

start = time()
df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
df1.join(df2, how="inner")
print(f"join耗时:{time() - start:.2f}s")
merge 耗时:4.68s
join 耗时:1.13s

确实默认参数情况下 join 比 merge 快4倍。

join函数的本质

但是为什么 join 会比 merge 快呢?pandas开发者总不能开发两套完全不相同的处理逻辑把?所以我们点开源码一探究竟。

下面我的pandas环境是基于 1.5.1 版本进行测试。

这里我使用pycharm通过Ctrl+点击对应函数查看源码,首先看看join的核心源码:

def join(
        self,
        other: DataFrame | Series | list[DataFrame | Series],
        on: IndexLabel | None = None,
        how: str = "left",
        lsuffix: str = "",
        rsuffix: str = "",
        sort: bool = False,
        validate: str | None = None,
)
 -> DataFrame:

    return self._join_compat(
        other,
        on=on,
        how=how,
        lsuffix=lsuffix,
        rsuffix=rsuffix,
        sort=sort,
        validate=validate,
    )


def _join_compat(
        self,
        other: DataFrame | Series,
        on: IndexLabel | None = None,
        how: str = "left",
        lsuffix: str = "",
        rsuffix: str = "",
        sort: bool = False,
        validate: str | None = None,
)
:

    from pandas.core.reshape.merge import merge
    return merge(
        self,
        other,
        left_on=on,
        how=how,
        left_index=on is None,
        right_index=True,
        suffixes=(lsuffix, rsuffix),
        sort=sort,
        validate=validate,
    )

咦, join 不就是最终依然调用 merge 方法吗?

再看看 merge 的源码:
def merge(
        self,
        right: DataFrame | Series,
        how: str = "inner",
        on: IndexLabel | None = None,
        left_on: IndexLabel | None = None,
        right_on: IndexLabel | None = None,
        left_index: bool = False,
        right_index: bool = False,
        sort: bool = False,
        suffixes: Suffixes = ("_x", "_y"),
        copy: bool = True,
        indicator: bool = False,
        validate: str | None = None,
)
 -> DataFrame:

    from pandas.core.reshape.merge import merge
    return merge(
        self,
        right,
        how=how,
        on=on,
        left_on=left_on,
        right_on=right_on,
        left_index=left_index,
        right_index=right_index,
        sort=sort,
        suffixes=suffixes,
        copy=copy,
        indicator=indicator,
        validate=validate,
    )

merge 也是调用了 pandas.core.reshape.merge 中的 merge 方法。

结论 : join 本质上是 merge 传入了 left_index 和 right_index 为True。

再次测试

基于上述结论,我们再次测试一下:

df1 = create_df("x")
df2 = create_df("y")

df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
df1.merge(df2, on="idx")
print(f"merge on耗时:{time() - start:.2f}s")

start = time()
df1.merge(df2, left_index=True, right_index=True)
print(f"merge index耗时:{time() - start:.2f}s")

start = time()
df1.join(df2, how="inner")
print(f"join耗时:{time() - start:.2f}s")
merge on耗时:4.30s
merge index耗时:1.15s
join耗时:0.11s

merge index 依然比 merge on 快了4倍,可是 join 为啥又比 merge index 快10倍?按照前面的分析, join 多调用了2层,应该比 merge index 更慢才对呀?难道是因为缓存?

现在我们调换一下 merge index 和 join 的执行顺序再试试:

df1 = create_df("x")
df2 = create_df("y")
df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)

start = time()
df1.join(df2, how="inner")
print(f"join耗时:{time() - start:.2f}s")

start = time()
df1.merge(df2, left_index=True, right_index=True)
print(f"merge index耗时:{time() - start:.2f}s")
join耗时:1.12s
merge index耗时:0.13s

结果这次变成merge index比join快10倍了。那么我们干脆让数据都重新生成后再测试:

df1 = create_df("x")
df2 = create_df("y")
df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
df1.join(df2, how="inner")
print(f"join耗时:{time() - start:.2f}s")

df1 = create_df("x")
df2 = create_df("y")
df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
df1.merge(df2, left_index=True, right_index=True)
print(f"merge index耗时:{time() - start:.2f}s")
join耗时:1.21s
merge index耗时:1.10s

这次相差不大,merge index比join略快一点,符合对源码的认知。但是明显第二次join或merge时,会比第一次快10倍,初步推测肯定是存在某种缓存。

缓存存在的位置

在join函数打上断点后,一步步内层跟,发现如下缓存位置:

而self是 Int64Index 对象,说明索引对象缓存是否具备唯一性的属性。索引是否具备唯一性决定了索引连接的形式:

我们可以再对比前先判断索引唯一性再测试:

df1 = create_df("x")
df2 = create_df("y")
df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
print(df1.index.is_unique, df2.index.is_unique)
print(f"索引判断唯一性耗时:{time() - start:.2f}s")

time1s = []
time2s = []
repeat = 10
for _ in range(repeat):
    start = time()
    df1.join(df2)
    time2s.append(time() - start)

    start = time()
    df1.merge(df2, left_index=True, right_index=True)
    time1s.append(time() - start)

print(f"merge index耗时:{sum(time1s) / repeat :.2f}s")
print(f"join耗时:{sum(time2s) / repeat :.2f}s")
True True
索引判断唯一性耗时:0.99s
merge index耗时:0.12s
join耗时:0.12s

可以看到索引判断唯一性耗时占了大头,取消这部分耗时后,两者耗时相差无几。

索引不唯一时速度对比

以上对比都是基于索引唯一的情景下,这次我们重新设计数据,假设索引存在五分之一的重复:

import pandas as pd
import numpy as np
from time import time


def create_df(col_prefix, nrows=100_0000):
    data = np.random.randint(0, 255, size=(nrows, 4))
    df = pd.DataFrame(data, columns=map(f"{col_prefix}_{{}}".format, range(4)))
    df["idx"] = np.random.choice(np.arange(nrows * 4 // 5), nrows)
    return df

再次测试:

start = time()
df1.merge(df2, on="idx")
print(f"merge on耗时:{time() - start:.2f}s")

df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
print(df1.index.is_unique, df2.index.is_unique)
print(f"索引判断唯一性耗时:{time() - start:.2f}s")

start = time()
df1.merge(df2, left_index=True, right_index=True)
print(f"merge index耗时:{time() - start:.2f}s")

start = time()
df1.join(df2, how="inner")
print(f"join耗时:{time() - start:.2f}s")

df1.reset_index(inplace=True)
df2.reset_index(inplace=True)
start = time()
df1.merge(df2, on="idx")
print(f"merge on耗时:{time() - start:.2f}s")
merge on耗时:0.53s
False False
索引判断唯一性耗时:0.06s
merge index耗时:0.91s
join耗时:0.93s
merge on耗时:0.50s
可以看到,在索引存在重复时,直接 merge on 反而更快。

但是不对劲啊,云朵君的测试明显是基于索引重复的情况下,得到 join 比 merge on 快的结论。其实这是因为云朵君的测试数据中,数据的重复度过大导致的,并不符合实际的数据情况。

云朵君给出的测试数据中,索引几乎达到100倍的重复,就是一个id能够重复出现100次左右。假如我们的测试也调整到10倍的索引重复度差距,结论就会不一样:

import pandas as pd
import numpy as np
from time import time


def create_df(col_prefix, nrows=100_0000):
    data = np.random.randint(0, 255, size=(nrows, 4))
    df = pd.DataFrame(data, columns=map(f"{col_prefix}_{{}}".format, range(4)))
    df["idx"] = np.random.choice(np.arange(nrows // 10), nrows)
    return df


df1 = create_df("x")
df2 = create_df("y")

start = time()
df1.merge(df2, how="left", on="idx")
print(f"merge on耗时:{time() - start:.2f}s")

df1.set_index("idx", inplace=True)
df2.set_index("idx", inplace=True)
start = time()
df1.join(df2)
print(f"join耗时:{time() - start:.2f}s")

df1.reset_index(inplace=True)
df2.reset_index(inplace=True)
start = time()
df1.merge(df2, how="left", on="idx")
print(f"merge on耗时:{time() - start:.2f}s")
merge on耗时:1.58s
join耗时:1.42s
merge on耗时:1.64s

很明显的看到这时join就比merge on快了。

如果提升到50倍:

merge on耗时:6.02s
join耗时:4.16s
merge on耗时:6.95s

结论

  1. join等价于merge传入left_index和right_index参数为True
  2. 索引唯一时,join比merge默认的on指定字段速度快
  3. 索引不唯一时,少量重复的索引时,merge默认的on指定字段比join快;每个索引能够重复10次以上时,join比merge默认的on指定字段快

🏴‍☠️宝藏级🏴‍☠️ 原创公众号『 数据STUDIO 』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括 可戳 👉 Python | MySQL | 数据分析 | 数据可视化 | 机器学习与数据挖掘 | 爬虫 等,从入门到进阶!
长按👇关注- 数据STUDIO -设为星标,干货速递