【问题标题】:What is the running time (big "O" order) of pandas DataFrame.join?pandas DataFrame.join 的运行时间(大“O”顺序)是多少?
【发布时间】:2023-03-25 01:06:02
【问题描述】:

这个问题更具概念性/理论性(与非常大的数据集的运行时间有关),因此我很抱歉没有展示一个最小的示例。

我有一堆来自两个不同传感器的数据帧,我需要最终将它们连接成来自两个不同传感器(df_snsr1df_snsr2)的两个非常大数据帧,然后左连接到单个数据帧。我的数据是这样的,我也可以先加入,然后再连接,或某种组合。我正在尝试找出最有效的方法。

通过阅读this SO answer,我知道pandas.concat 为其所有数据帧的连接分配空间,如果您在循环中执行此操作,可能会导致O(N**2) 复制和一些主要的减速。因此,我目前首先构建一个大数据帧列表(从文件加载),一次将它们连接起来,然后加入两个大数据帧:

df_list = []
for file in my_pickle_files_snsr1:  # O(M) loop over M files
    df_list.append(pd.read_pickle(file))  # O(1) append, M times
df_snsr1 = pd.concat(df_list)  # O(N) copies of N records
# repeat for sensor 2 (df_snsr2)
df_snsr1.join(df_snsr2, on=['some', 'columns'])  # O(dunno, maybe bears?)

我在pandas.DataFrame.join 的文档中找不到任何有关执行速度的信息。是O(N)吗? O(N**2)?我的想法是,如果它与pandas.concat 的顺序相似,那么我执行这两个操作的顺序实际上并不重要。但是,如果是O(N**2),那么我加入可能会更有效率许多小的数据帧,然后将它们连接起来,而不是连接然后加入。整个操作需要足够长的时间,值得我在这里提出问题,所以“运行它并查看”是行不通的。

有人知道join 使用的是什么算法以及它的执行大O 顺序是什么吗?或者有人对获得joinconcat 的最有效组合有任何其他建议吗?

【问题讨论】:

  • 虽然我也对您的问题的答案感兴趣,但我建议您查看 dask 提供的solution 正是针对此问题(即,将大量文件读入一个 DataFrame)。它并不真正支持读取大量 pickle 文件,但 csv、parquet、hdf 和许多其他文件类型真的很容易以这种方式读取。 import dask.dataframe as dd; df_snsr1 = dd.read_csv(list_of_csv_files_or_path_regex); df_snsr1 = df_snsr1.compute()

标签: python pandas dataframe big-o execution-time


【解决方案1】:

我认为这取决于您传递给join 的选项(例如连接类型和是否排序)。

当使用默认的how='left'时,结果似乎是排序的,至少对于单个索引(文档只指定了某些how方法的输出顺序, inner 不是其中之一)。在任何情况下,排序都是O(n log n)。每个索引查找都是O(1),其中有O(n)。因此,在这种情况下,O(n log n) 占主导地位。

相比之下,在 how='inner' 的情况下,指定保持调用 DataFrame 的顺序。在这种情况下,我们会期望 O(n)(既用于可能的集合交集,也用于索引查找和插入)。

在任何一种情况下,随着大小变得越来越大,缓存局部性(或缺乏)的各种问题开始向您蔓延,并且在随机访问中访问大内存区域所花费的实际时间将开始占主导地位。以上仅针对操作复杂度。

正如其他地方提到的,对于更大的数据集,Dask 是一种可行的方法,或者 Spark。


但是你说我们测试它(至少how='left' 案例)是什么?下面的代码比我希望的要冗长一些(而且名称生成很愚蠢),但它就是这样做的。本质上,它生成了两个具有随机名称的 DF,unordered,并且具有共同的 1 - replace_fraction 分数;然后它在测量所用时间的同时加入它们。

from IPython.core.magics.execution import _format_time as walltime

def make_names(n):
    names = [
        f'{x}{y}{z}' for (x, y), z in zip(
            np.random.choice(['foo', 'bar', 'hi'], (n, 2)),
            np.random.randint(0, n, size=n))
    ]
    return names

def work(n, replace_fraction=0.1):
    a_names = make_names(n)
    replace_n = int(n * replace_fraction)
    b_names = make_names(replace_n) + list(np.random.choice(a_names, size=n - replace_n, replace=False))
    np.random.shuffle(b_names)
    a = pd.DataFrame({
        'name': a_names,
        'v': np.random.uniform(size=n),
        'w': np.random.uniform(size=n),
    }).set_index('name')
    b = pd.DataFrame({
        'name': b_names,
        'v': np.random.uniform(size=n),
        'w': np.random.uniform(size=n),
    }).set_index('name')

    t0 = time.time()
    df = a.join(b, rsuffix='_r')
    dt = time.time() - t0
    return a, b, df, dt

例如:试试work(4, .5)

现在,对几何尺寸系列进行一些时间测量:

sizes = (2**np.arange(10, 23, .5)).astype(int)
times = []
for n in sizes:
    a, b, df, dt = work(n)
    times.append(dt)
    print(f'{n}: {walltime(dt)}')

# out:
1024: 2.9 ms
1448: 4.78 ms
2048: 4.37 ms
...
2965820: 18.2 s
4194304: 30.2 s
5931641: 44.8 s

适合n log n

from numpy.polynomial.polynomial import polyfit

n = np.array(sizes)
t = np.array(times)
b, m = polyfit(n * np.log(n), t, 1)

plt.plot(n/1e6, t, '.')
plt.plot(n/1e6, b + m * n * np.log(n), '-')
plt.xlabel('size [M]')
plt.ylabel('time [s]')
plt.show()

(旁注:scipy.optimize.nnls 与所有术语 nlog nn log n1 找到除 n log n 之外的所有系数 0,因此上述情况很好)。

【讨论】:

  • 哇,很棒的分析!非常感谢!
猜你喜欢
  • 2018-12-31
  • 1970-01-01
  • 1970-01-01
  • 2015-12-19
  • 1970-01-01
  • 2014-04-19
  • 1970-01-01
  • 1970-01-01
  • 2014-05-01
相关资源
最近更新 更多