前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >错得离谱!竟然说pandas中的join比merge快5倍?我带你看源码吧

错得离谱!竟然说pandas中的join比merge快5倍?我带你看源码吧

作者头像
咋咋
发布2023-02-10 12:03:21
8450
发布2023-02-10 12:03:21
举报
文章被收录于专栏:数据大宇宙数据大宇宙

前言

最近有两位小伙伴跟我说,网上看到一篇文章说,在 python 中使用 pandas 连接两个表,别用 merge ,要使用 join,因为在大量数据的情况下 join 比 merge 要快4到5倍。

其实这说法我一听就知道是错误的。不过当时没有具体证据支持,所以我也没有下具体结论。

今天,我就从源码的角度,给大家一个参考依据。

当然,本文你还会学到一些代码调试技巧,还会看到一些 pandas 的优化手段。


join 比 merge 快很多?

那篇文章中的测试大概如下:

代码语言:javascript
复制
import pandas as pd
import numpy as np
from time import time

high = 1000
rows_list = [(i + 1) * 1_000_000 for i in range(10)]
n_columns = 4
repeat = 5

def create_df(n_rows, n_columns, col_names):
    data = np.random.randint(low=-high, high=high, size=(n_rows, n_columns))
    index_col = np.arange(0, n_rows)
    np.random.shuffle(index_col)
    data = pd.DataFrame(data, columns=col_names, dtype=np.int16)
    data["idx"] = index_col
    return data
代码语言:javascript
复制
for n_rows in [10_000_000]:
    sum_time_merge1 = 0
    sum_time_merge2 = 0

    for _ in range(repeat):
        df1 = create_df(n_rows, n_columns, [f"col_{i}" for i in range(n_columns)])
        df2 = create_df(n_rows, n_columns, [f"Col_{i}" for i in range(n_columns)])

        # merge
        start = time()
        df = pd.merge(df1, df2, how="left",left_on = 'col_0',right_on='Col_0')
        sum_time_merge1 += time() - start

        ## join
        start = time()
        df1.set_index("idx", inplace=True)
        df2.set_index("idx", inplace=True)
        df = df1.join(df2)
        sum_time_merge2 += time() - start

    result.append([df1.shape[0], sum_time_merge1 / repeat, sum_time_merge2 / repeat])

print(pd.DataFrame(result, columns=["行数", "merge耗时(秒)", "join耗时(秒)"]))
  • 跑一千万数据,5次,取个平均
  • 使用 df.join 有个前提,把2个表的关联key的列设置为行索引
  • merge 则使用普通的列作为关联key
  • 我这里生成的key 是唯一的。足以复现原文的效果

看看结果:

嗯?还真快了这么多!

但是为什么我一开始听到这说法,不用做任何的实验,就觉得这观点有问题?

其实道理很简单。

假如今天你实现了一个功能函数:

  • 功能很简单,把一个列表中的数值,先转成正数,然后求和

明天,你需要实现另一个功能很接近的函数,只不过输入的不是列表,而是2个具体的数值。显然你会想着调用之前的函数:

同样道理,join 函数明显是 merge 函数的一个特例。pandas 的设计者不会傻到用两套不一样的方式实现它们。

但是,别人给出来的实验结果确确实实反应了它们的差异。

接下来,我们就看看它们实现的源码。


源码找答案

首先,新建一个 python 文件,把代码设置得简单一些。

打开调试窗口,点击创建 python 的调试配置。

  • 这里最重要的是设置 justMyCode 为 false 。这样子我们才能进入 pandas 源码里面

接着,在 merge 函数那一行打开一个断点

执行调试

代码会停在断点的行,接着我们要点击控制菜单中的下一步(也可以用快捷键)。

可以看到,merge 函数实际调用的是 pandas.core.reshape.merge.merge ,暂时不深入

如果你看过我之前关于类定义的文章,那么不用看里面的实现也知道,这里只不过实例化了一个对象,记录了一些相关数据而已,重要的是下方的 get result 函数

同样道理,调试 join 函数

咦?它的实现与 merge 不一样?别急,继续执行,直到

进入一看,又跳回到之前 merge 函数的实现

从左侧的调用堆栈中可以看到调用顺序:

  • 1 是join调用
  • 2 是 join compat
  • 3 就是上图右边的代码

你可以点击调用堆栈中的一行,代码会跳回去,就连当时执行中的所有变量的值都可以查看

简单列一下大概的调用图:

  • join 函数绕了一圈才到真正执行的地方

所以现在我们知道,join 函数其实比 merge 函数执行更多的代码。

但是,之前的实验数据不是很好说明了 join 比 merge 快呀,为什么?


不公平的对比

按调试流程,我们进入之前看到的 op.get result 函数里面:

进入这个 self._get_join_info() 里面:

可以看到许多关于 left index 和 right index 参数的判断。但是 我们使用 merge 的时候根本没有设置这两个参数,它们都是 False。

结果就会进入这段代码:

这是一个 python 的遍历代码,一个个去匹配 key 值

而 join 函数执行的却是:

  • 直接调用行索引对象的函数

了解这些要点,相信聪明的你也知道要这样子修改实验代码:

  • 把设置行索引的代码移除两个函数执行的范围外
  • merge 设置参数 left index 与 right index

但是,结果却出乎意料!!

对比一下之前的时间:

解释一下差异:

  1. join 的耗时短了很多,因为现在它没有设置行索引的操作
  2. merge 耗时也短了很多,因为现在它内部用了行索引

但是,为什么 merge 耗时仍然比 join 要慢很多?


pandas 的优化

此时,我们把实验代码中执行 merge 和 join 的先后顺序调换一下:

  • 注意,记录时间的变量的对应关系没有变,所以这不会影响结果表格的左右顺序

看看结果:

现在,结论截然相反!

为什么?显然,有什么东西在第二次运行的时候,得到了优化。

在之前的源码调试中,我们得知,其实两个表按行索引关联,最核心的计算就是行索引对象的 join 函数。

按这个原理以及之前的调试方式,可以找到一个属性。具体过程我就不再啰嗦了,直接给出验证结果:

在 join 的过程中,有一个判断逻辑,如果行索引的值都是唯一的,那么会进行一些操作。

直接看看它的源码

缓存了结果。

道理很简单, pandas 怎么可以知道一个行索引的值是否唯一?显然要遍历一次数据。这个过程在大量数据的时候成本很高。由于索引对象是不可变的,所以可以缓存结果。

那么,现在我们修正一下测试实验的代码,让它公平对待:

现在的结果是:

很多小伙伴问我怎么学习 pandas 。正如我专栏里面的思路,集中学习少数核心常用的函数和原理,你的学习之路才能事半功倍。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据大宇宙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • join 比 merge 快很多?
  • 源码找答案
  • 不公平的对比
  • pandas 的优化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档