两个DataFrame中每行和每列的差异(Python / Pandas)

2 投票
1 回答
1817 浏览
提问于 2025-04-19 03:05

有没有更高效的方法来比较一个数据框(DF)中每一行的每一列和另一个数据框中每一行的每一列?我觉得这样做有点乱,但我尝试用循环或应用函数的方法速度都慢得多。

df1 = pd.DataFrame({'a': np.random.randn(1000),
                   'b': [1, 2] * 500,
                   'c': np.random.randn(1000)},
                   index=pd.date_range('1/1/2000', periods=1000))
df2 = pd.DataFrame({'a': np.random.randn(100),
                'b': [2, 1] * 50,
                'c': np.random.randn(100)},
               index=pd.date_range('1/1/2000', periods=100))
df1 = df1.reset_index()
df1['embarrassingHackInd'] = 0
df1.set_index('embarrassingHackInd', inplace=True)
df1.rename(columns={'index':'origIndex'}, inplace=True)
df1['df1Date'] = df1.origIndex.astype(np.int64) // 10**9
df1['df2Date'] = 0
df2 = df2.reset_index()
df2['embarrassingHackInd'] = 0
df2.set_index('embarrassingHackInd', inplace=True)
df2.rename(columns={'index':'origIndex'}, inplace=True)
df2['df2Date'] = df2.origIndex.astype(np.int64) // 10**9
df2['df1Date'] = 0
timeit df3 = abs(df1-df2)

进行了10次循环,最好的结果是每次循环60.6毫秒。

我需要知道每次比较是怎么进行的,所以我不得不把每个相对的索引加到比较的数据框中,这样才能在最终的数据框里看到。

提前感谢任何帮助。

1 个回答

6

你发的代码展示了一种聪明的方法来生成减法表。不过,这种方法并没有充分利用Pandas的优势。Pandas的数据框(DataFrame)是以列为基础来存储数据的,所以按列获取数据的速度是最快的,而不是按行。因为所有的行都有相同的索引,减法是按行进行的(将每一行与其他每一行配对),这就意味着在df1-df2中会有很多按行的数据获取。这对Pandas来说并不是最理想的,特别是当并不是所有的列都有相同的数据类型时。

减法表是NumPy擅长的事情:

In [5]: x = np.arange(10)

In [6]: y = np.arange(5)

In [7]: x[:, np.newaxis] - y
Out[7]: 
array([[ 0, -1, -2, -3, -4],
       [ 1,  0, -1, -2, -3],
       [ 2,  1,  0, -1, -2],
       [ 3,  2,  1,  0, -1],
       [ 4,  3,  2,  1,  0],
       [ 5,  4,  3,  2,  1],
       [ 6,  5,  4,  3,  2],
       [ 7,  6,  5,  4,  3],
       [ 8,  7,  6,  5,  4],
       [ 9,  8,  7,  6,  5]])

你可以把x看作是df1的一列,把y看作是df2的一列。你会发现,NumPy可以用基本相同的语法处理df1df2的所有列。


下面的代码定义了origusing_numpyorig是你发的代码,using_numpy是使用NumPy数组进行减法的另一种方法:

In [2]: %timeit orig(df1.copy(), df2.copy())
10 loops, best of 3: 96.1 ms per loop

In [3]: %timeit using_numpy(df1.copy(), df2.copy())
10 loops, best of 3: 19.9 ms per loop

import numpy as np
import pandas as pd
N = 100
df1 = pd.DataFrame({'a': np.random.randn(10*N),
                   'b': [1, 2] * 5*N,
                   'c': np.random.randn(10*N)},
                   index=pd.date_range('1/1/2000', periods=10*N))
df2 = pd.DataFrame({'a': np.random.randn(N),
                'b': [2, 1] * (N//2),
                'c': np.random.randn(N)},
               index=pd.date_range('1/1/2000', periods=N))

def orig(df1, df2):
    df1 = df1.reset_index() # 312 µs per loop
    df1['embarrassingHackInd'] = 0 # 75.2 µs per loop
    df1.set_index('embarrassingHackInd', inplace=True) # 526 µs per loop
    df1.rename(columns={'index':'origIndex'}, inplace=True) # 209 µs per loop
    df1['df1Date'] = df1.origIndex.astype(np.int64) // 10**9 # 23.1 µs per loop
    df1['df2Date'] = 0

    df2 = df2.reset_index()
    df2['embarrassingHackInd'] = 0
    df2.set_index('embarrassingHackInd', inplace=True)
    df2.rename(columns={'index':'origIndex'}, inplace=True)
    df2['df2Date'] = df2.origIndex.astype(np.int64) // 10**9
    df2['df1Date'] = 0
    df3 = abs(df1-df2) # 88.7 ms per loop  <-- this is the bottleneck
    return df3

def using_numpy(df1, df2):
    df1.index.name = 'origIndex'
    df2.index.name = 'origIndex'
    df1.reset_index(inplace=True) 
    df2.reset_index(inplace=True) 
    df1_date = df1['origIndex']
    df2_date = df2['origIndex']
    df1['origIndex'] = df1_date.astype(np.int64) 
    df2['origIndex'] = df2_date.astype(np.int64) 

    arr1 = df1.values
    arr2 = df2.values
    arr3 = np.abs(arr1[:,np.newaxis,:]-arr2) # 3.32 ms per loop vs 88.7 ms 
    arr3 = arr3.reshape(-1, 4)
    index = pd.MultiIndex.from_product(
        [df1_date, df2_date], names=['df1Date', 'df2Date'])
    result = pd.DataFrame(arr3, index=index, columns=df1.columns)
    # You could stop here, but the rest makes the result more similar to orig
    result.reset_index(inplace=True, drop=False)
    result['df1Date'] = result['df1Date'].astype(np.int64) // 10**9
    result['df2Date'] = result['df2Date'].astype(np.int64) // 10**9
    return result

def is_equal(expected, result):
    expected.reset_index(inplace=True, drop=True)
    result.reset_index(inplace=True, drop=True)

    # expected has dtypes 'O', while result has some float and int dtypes. 
    # Make all the dtypes float for a quick and dirty comparison check
    expected = expected.astype('float')
    result = result.astype('float')
    columns = ['a','b','c','origIndex','df1Date','df2Date']
    return expected[columns].equals(result[columns])

expected = orig(df1.copy(), df2.copy())
result = using_numpy(df1.copy(), df2.copy())
assert is_equal(expected, result)

x[:, np.newaxis] - y是如何工作的:

这个表达式利用了NumPy的广播功能。要理解广播,首先要知道数组的形状:

In [6]: x.shape
Out[6]: (10,)

In [7]: x[:, np.newaxis].shape
Out[7]: (10, 1)

In [8]: y.shape
Out[8]: (5,)

[:, np.newaxis]是在右边x添加了一个新轴,所以它的形状变成了(10, 1)。因此,x[:, np.newaxis] - y就是一个形状为(10, 1)的数组与一个形状为(5,)的数组进行减法。

乍一看,这似乎不太合理,但NumPy数组会根据某些规则广播它们的形状,以尝试使它们的形状兼容。

第一个规则是可以在左边添加新轴。所以一个形状为(5,)的数组可以广播成形状为(1, 5)

下一个规则是长度为1的轴可以广播成任意长度。数组中的值会在额外的维度上重复,直到满足需要。

因此,当形状为(10, 1)(1, 5)的数组在NumPy的算术操作中结合时,它们都会被广播成形状为(10, 5)的数组:

In [14]: broadcasted_x, broadcasted_y = np.broadcast_arrays(x[:, np.newaxis], y)

In [15]: broadcasted_x
Out[15]: 
array([[0, 0, 0, 0, 0],
       [1, 1, 1, 1, 1],
       [2, 2, 2, 2, 2],
       [3, 3, 3, 3, 3],
       [4, 4, 4, 4, 4],
       [5, 5, 5, 5, 5],
       [6, 6, 6, 6, 6],
       [7, 7, 7, 7, 7],
       [8, 8, 8, 8, 8],
       [9, 9, 9, 9, 9]])

In [16]: broadcasted_y
Out[16]: 
array([[0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4]])

所以x[:, np.newaxis] - y等同于broadcasted_x - broadcasted_y

现在,掌握了这个简单的例子后,我们可以看看arr1[:,np.newaxis,:]-arr2

arr1的形状是(1000, 4),而arr2的形状是(100, 4)。我们想要在长度为4的轴上进行减法,针对每一行沿着1000长度的轴和每一行沿着100长度的轴。换句话说,我们想要生成一个形状为(1000, 100, 4)的减法结果。

重要的是,我们不希望1000轴100轴相互作用。我们希望它们在不同的轴上

所以如果我们像这样给arr1添加一个轴:arr1[:,np.newaxis,:],那么它的形状就变成了

In [22]: arr1[:, np.newaxis, :].shape
Out[22]: (1000, 1, 4)

现在,NumPy广播将两个数组都扩展到共同的形状(1000, 100, 4)。太好了,减法表就出来了。

为了将这些值整理成一个形状为(1000*100, 4)的二维数据框,我们可以使用reshape

arr3 = arr3.reshape(-1, 4)

-1告诉NumPy用所需的正整数替代-1,以使重塑合理。由于arr有1000*100*4个值,-1会被替换为1000*100。使用-1比直接写1000*100要好,因为这样即使我们改变df1df2的行数,代码仍然可以正常工作。

撰写回答